pub mod sprite; mod watcher; use self::sprite::SpriteSpec; pub use self::watcher::{BlocksOfInterest, FireplaceType, Interaction}; use crate::{ mesh::{ greedy::{GreedyMesh, SpriteAtlasAllocator}, segment::generate_mesh_base_vol_sprite, terrain::{generate_mesh, SUNLIGHT, SUNLIGHT_INV}, }, render::{ pipelines::{self, AtlasData, AtlasTextures}, AltIndices, CullingMode, FigureSpriteAtlasData, FirstPassDrawer, FluidVertex, GlobalModel, Instances, LodData, Mesh, Model, RenderError, Renderer, SpriteDrawer, SpriteGlobalsBindGroup, SpriteInstance, SpriteVertex, SpriteVerts, TerrainAtlasData, TerrainLocals, TerrainShadowDrawer, TerrainVertex, SPRITE_VERT_PAGE_SIZE, }, scene::terrain::sprite::SpriteModelConfig, }; use super::{ camera::{self, Camera}, math, SceneData, RAIN_THRESHOLD, }; use common::{ assets::{AssetExt, DotVoxAsset}, figure::Segment, spiral::Spiral2d, terrain::{Block, SpriteKind, TerrainChunk}, vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol}, volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError}, }; use common_base::{prof_span, span}; use core::{f32, fmt::Debug, marker::PhantomData, time::Duration}; use crossbeam_channel as channel; use guillotiere::AtlasAllocator; use hashbrown::HashMap; use std::sync::{ atomic::{AtomicU64, Ordering}, Arc, }; use strum::IntoEnumIterator; use tracing::warn; use treeculler::{BVol, Frustum, AABB}; use vek::*; const SPRITE_SCALE: Vec3 = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0); pub const SPRITE_LOD_LEVELS: usize = 5; // For rain occlusion we only need to render the closest chunks. /// How many chunks are maximally rendered for rain occlusion. pub const RAIN_OCCLUSION_CHUNKS: usize = 25; #[derive(Clone, Copy, Debug)] struct Visibility { in_range: bool, in_frustum: bool, } impl Visibility { /// Should the chunk actually get rendered? fn is_visible(&self) -> bool { // Currently, we don't take into account in_range to allow all chunks to do // pop-in. This isn't really a problem because we no longer have VD mist // or anything like that. Also, we don't load chunks outside of the VD // anyway so this literally just controls which chunks get actually // rendered. /* self.in_range && */ self.in_frustum } } /// Type of closure used for light mapping. type LightMapFn = Arc) -> f32 + Send + Sync>; pub struct TerrainChunkData { // GPU data load_time: f32, opaque_model: Option>, fluid_model: Option>, /// If this is `None`, this texture is not allocated in the current atlas, /// and therefore there is no need to free its allocation. atlas_alloc: Option, /// The actual backing texture for this chunk. Use this for rendering /// purposes. The texture is reference-counted, so it will be /// automatically freed when no chunks are left that need it (though /// shadow chunks will still keep it alive; we could deal with this by /// making this an `Option`, but it probably isn't worth it since they /// shouldn't be that much more nonlocal than regular chunks). atlas_textures: Arc>, light_map: LightMapFn, glow_map: LightMapFn, sprite_instances: [(Instances, AltIndices); SPRITE_LOD_LEVELS], locals: pipelines::terrain::BoundLocals, pub blocks_of_interest: BlocksOfInterest, visible: Visibility, can_shadow_point: bool, can_shadow_sun: bool, z_bounds: (f32, f32), sun_occluder_z_bounds: (f32, f32), frustum_last_plane_index: u8, alt_indices: AltIndices, } /// The depth at which the intermediate zone between underground and surface /// begins pub const SHALLOW_ALT: f32 = 24.0; /// The depth at which the intermediate zone between underground and surface /// ends pub const DEEP_ALT: f32 = 96.0; /// The depth below the surface altitude at which the camera switches from /// displaying surface elements to underground elements pub const UNDERGROUND_ALT: f32 = (SHALLOW_ALT + DEEP_ALT) * 0.5; // The distance (in chunks) within which all levels of the chunks will be drawn // to minimise cull-related popping. const NEVER_CULL_DIST: i32 = 3; #[derive(Copy, Clone)] struct ChunkMeshState { pos: Vec2, started_tick: u64, is_worker_active: bool, // If this is set, we skip the actual meshing part of the update. skip_remesh: bool, } /// Just the mesh part of a mesh worker response. pub struct MeshWorkerResponseMesh { z_bounds: (f32, f32), sun_occluder_z_bounds: (f32, f32), opaque_mesh: Mesh, fluid_mesh: Mesh, atlas_texture_data: TerrainAtlasData, atlas_size: Vec2, light_map: LightMapFn, glow_map: LightMapFn, alt_indices: AltIndices, } /// A type produced by mesh worker threads corresponding to the position and /// mesh of a chunk. struct MeshWorkerResponse { pos: Vec2, sprite_instances: [(Vec, AltIndices); SPRITE_LOD_LEVELS], /// If None, this update was requested without meshing. mesh: Option, started_tick: u64, blocks_of_interest: BlocksOfInterest, } pub fn get_sprite_instances<'a, I: 'a>( lod_levels: &'a mut [I; SPRITE_LOD_LEVELS], set_instance: impl Fn(&mut I, SpriteInstance, Vec3), blocks: impl Iterator, Block)>, mut to_wpos: impl FnMut(Vec3) -> Vec3, mut light_map: impl FnMut(Vec3) -> f32, mut glow_map: impl FnMut(Vec3) -> f32, sprite_data: &HashMap<(SpriteKind, usize, usize), [SpriteData; SPRITE_LOD_LEVELS]>, sprite_config: &SpriteSpec, ) { prof_span!("extract sprite_instances"); for (rel_pos, block) in blocks { let Some(sprite) = block.get_sprite() else { continue; }; let Some((cfg_i, cfg)) = sprite_config.get_for_block(&block) else { continue; }; let wpos = to_wpos(rel_pos); let seed = (wpos.x as u64) .overflowing_mul(3) .0 .overflowing_add((wpos.y as u64).overflowing_mul(7).0) .0 .overflowing_add((wpos.x as u64).overflowing_mul(wpos.y as u64).0) .0; // Awful PRNG // % 4 is non uniform, take 7 and combine two lesser probable outcomes let ori = (block.get_ori().unwrap_or((((seed % 7) + 1) / 2) as u8 * 2)) & 0b111; if !cfg.variations.is_empty() { // try to make the variation more uniform as the PRNG is highly unfair let variation = match cfg.variations.len() { 1 => 0, 2 => (seed as usize % 4) / 3, 3 => (seed as usize % 5) / 2, // for four use a different seed than for ori to not have them match always 4 => (((seed.overflowing_add(wpos.x as u64).0) as usize % 7) + 1) / 2, _ => seed as usize % cfg.variations.len(), }; let key = (sprite, variation, cfg_i); // NOTE: Safe because we called sprite_config_for already. // NOTE: Safe because 0 ≤ ori < 8 let light = light_map(wpos); let glow = glow_map(wpos); for (lod_level, sprite_data) in lod_levels.iter_mut().zip(&sprite_data[&key]) { let mat = Mat4::identity() // Scaling for different LOD resolutions .scaled_3d(sprite_data.scale) // Offset .translated_3d(sprite_data.offset) .scaled_3d(SPRITE_SCALE) .rotated_z(f32::consts::PI * 0.25 * ori as f32) .translated_3d( rel_pos + Vec3::new(0.5, 0.5, 0.0) ); // Add an instance for each page in the sprite model for page in sprite_data.vert_pages.clone() { // TODO: could be more efficient to create once and clone while // modifying vert_page let instance = SpriteInstance::new( mat, cfg.wind_sway, sprite_data.scale.z, rel_pos.as_(), ori, light, glow, page, sprite.is_door(), ); set_instance(lod_level, instance, wpos); } } } } } /// Function executed by worker threads dedicated to chunk meshing. /// skip_remesh is either None (do the full remesh, including recomputing the /// light map), or Some((light_map, glow_map)). fn mesh_worker( pos: Vec2, z_bounds: (f32, f32), skip_remesh: Option<(LightMapFn, LightMapFn)>, started_tick: u64, volume: as SampleVol>>::Sample, max_texture_size: u16, chunk: Arc, range: Aabb, sprite_data: &HashMap<(SpriteKind, usize, usize), [SpriteData; SPRITE_LOD_LEVELS]>, sprite_config: &SpriteSpec, ) -> MeshWorkerResponse { span!(_guard, "mesh_worker"); let blocks_of_interest = BlocksOfInterest::from_blocks( chunk.iter_changed().map(|(pos, block)| (pos, *block)), chunk.meta().river_velocity().magnitude_squared(), chunk.meta().temp(), chunk.meta().humidity(), &*chunk, ); let mesh; let (light_map, glow_map) = if let Some((light_map, glow_map)) = &skip_remesh { mesh = None; (&**light_map, &**glow_map) } else { let ( opaque_mesh, fluid_mesh, _shadow_mesh, ( bounds, atlas_texture_data, atlas_size, light_map, glow_map, alt_indices, sun_occluder_z_bounds, ), ) = generate_mesh( &volume, ( range, Vec2::new(max_texture_size, max_texture_size), &blocks_of_interest, ), ); mesh = Some(MeshWorkerResponseMesh { // TODO: Take sprite bounds into account somehow? z_bounds: (bounds.min.z, bounds.max.z), sun_occluder_z_bounds, opaque_mesh, fluid_mesh, atlas_texture_data, atlas_size, light_map, glow_map, alt_indices, }); // Pointer juggling so borrows work out. let mesh = mesh.as_ref().unwrap(); (&*mesh.light_map, &*mesh.glow_map) }; let to_wpos = |rel_pos: Vec3| { Vec3::from(pos * TerrainChunk::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos.as_() }; MeshWorkerResponse { pos, // Extract sprite locations from volume sprite_instances: { prof_span!("extract sprite_instances"); let mut instances = [(); SPRITE_LOD_LEVELS].map(|()| { ( Vec::new(), // Deep Vec::new(), // Shallow Vec::new(), // Surface ) }); let (underground_alt, deep_alt) = volume .get_key(volume.pos_key((range.min + range.max) / 2)) .map_or((0.0, 0.0), |c| { (c.meta().alt() - SHALLOW_ALT, c.meta().alt() - DEEP_ALT) }); get_sprite_instances( &mut instances, |(deep_level, shallow_level, surface_level), instance, wpos| { if (wpos.z as f32) < deep_alt { deep_level.push(instance); } else if wpos.z as f32 > underground_alt { surface_level.push(instance); } else { shallow_level.push(instance); } }, (0..TerrainChunk::RECT_SIZE.x as i32) .flat_map(|x| { (0..TerrainChunk::RECT_SIZE.y as i32).flat_map(move |y| { (z_bounds.0 as i32..z_bounds.1 as i32) .map(move |z| Vec3::new(x, y, z).as_()) }) }) .filter_map(|rel_pos| Some((rel_pos, *volume.get(to_wpos(rel_pos)).ok()?))), to_wpos, light_map, glow_map, sprite_data, sprite_config, ); instances.map(|(deep_level, shallow_level, surface_level)| { let deep_end = deep_level.len(); let alt_indices = AltIndices { deep_end, underground_end: deep_end + shallow_level.len(), }; ( deep_level .into_iter() .chain(shallow_level) .chain(surface_level) .collect(), alt_indices, ) }) }, mesh, blocks_of_interest, started_tick, } } pub struct SpriteData { // Sprite vert page ranges that need to be drawn vert_pages: core::ops::Range, // Scale scale: Vec3, // Offset offset: Vec3, } pub struct Terrain { /// This is always the *current* atlas into which data is being allocated. /// Once an atlas is too full to allocate the next texture, we always /// allocate a fresh texture and start allocating into that. Trying to /// keep more than one texture available for allocation doesn't seem /// worth it, because our allocation patterns are heavily spatial (so all /// data allocated around the same time should have a very similar lifetime, /// even in pathological cases). As a result, fragmentation effects /// should be minimal. /// /// TODO: Consider "moving GC" style allocation to deal with spatial /// fragmentation effects due to odd texture sizes, which in some cases /// might significantly reduce the number of textures we need for /// particularly difficult locations. atlas: AtlasAllocator, chunks: HashMap, TerrainChunkData>, /// Temporary storage for dead chunks that might still be shadowing chunks /// in view. We wait until either the chunk definitely cannot be /// shadowing anything the player can see, the chunk comes back into /// view, or for daylight to end, before removing it (whichever comes /// first). /// /// Note that these chunks are not complete; for example, they are missing /// texture data (they still currently hold onto a reference to their /// backing texture, but it generally can't be trusted for rendering /// purposes). shadow_chunks: Vec<(Vec2, TerrainChunkData)>, /* /// Secondary index into the terrain chunk table, used to sort through chunks by z index from /// the top down. z_index_down: BTreeSet>, /// Secondary index into the terrain chunk table, used to sort through chunks by z index from /// the bottom up. z_index_up: BTreeSet>, */ // The mpsc sender and receiver used for talking to meshing worker threads. // We keep the sender component for no reason other than to clone it and send it to new // workers. mesh_send_tmp: channel::Sender, mesh_recv: channel::Receiver, mesh_todo: HashMap, ChunkMeshState>, mesh_todos_active: Arc, mesh_recv_overflow: f32, // GPU data // Maps sprite kind + variant to data detailing how to render it pub sprite_render_state: SpriteRenderState, pub sprite_globals: SpriteGlobalsBindGroup, /// As stated previously, this is always the very latest texture into which /// we allocate. Code cannot assume that this is the assigned texture /// for any particular chunk; look at the `texture` field in /// `TerrainChunkData` for that. atlas_textures: Arc>, phantom: PhantomData, } impl TerrainChunkData { pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun } } #[derive(Clone)] pub struct SpriteRenderState { /// FIXME: This could possibly become an `AssetHandle`, to get /// hot-reloading for free, but I am not sure if sudden changes of this /// value would break something pub sprite_config: Arc, // Maps sprite kind + variant to data detailing how to render it pub sprite_data: Arc>, pub sprite_atlas_textures: Arc>, } #[derive(Clone)] pub struct SpriteRenderContext { pub state: SpriteRenderState, pub sprite_verts_buffer: Arc, } pub type SpriteRenderContextLazy = Box SpriteRenderContext>; impl SpriteRenderContext { pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy { let max_texture_size = renderer.max_texture_size(); struct SpriteWorkerResponse { sprite_config: Arc, sprite_data: HashMap<(SpriteKind, usize, usize), [SpriteData; SPRITE_LOD_LEVELS]>, sprite_atlas_texture_data: FigureSpriteAtlasData, sprite_atlas_size: Vec2, sprite_mesh: Mesh, } let join_handle = std::thread::spawn(move || { prof_span!("mesh all sprites"); // Load all the sprite config data. let sprite_config = Arc::::load_expect("voxygen.voxel.sprite_manifest").cloned(); let max_size = Vec2::from(u16::try_from(max_texture_size).unwrap_or(u16::MAX)); let mut greedy = GreedyMesh::::new( max_size, crate::mesh::greedy::sprite_config(), ); let mut sprite_mesh = Mesh::new(); // NOTE: Tracks the start vertex of the next model to be meshed. let sprite_data: HashMap<(SpriteKind, usize, usize), _> = SpriteKind::iter() .flat_map(|kind| { sprite_config .get(kind) .enumerate() .map(move |(i, (v, _))| (kind, v, i)) }) .flat_map(|(kind, sprite_config, filter_variant)| { sprite_config.variations.iter().enumerate().map( move |( variation, SpriteModelConfig { model, offset, lod_axes, }, )| { let scaled = [1.0, 0.8, 0.6, 0.4, 0.2]; let offset = Vec3::from(*offset); let lod_axes = Vec3::from(*lod_axes); let model = DotVoxAsset::load_expect(model); let zero = Vec3::zero(); let model_size = model .read() .0 .models .first() .map( |&dot_vox::Model { size: dot_vox::Size { x, y, z }, .. }| Vec3::new(x, y, z), ) .unwrap_or(zero); let max_model_size = Vec3::new(31.0, 31.0, 63.0); let model_scale = max_model_size.map2(model_size, |max_sz: f32, cur_sz| { let scale = max_sz / max_sz.max(cur_sz as f32); if scale < 1.0 && (cur_sz as f32 * scale).ceil() > max_sz { scale - 0.001 } else { scale } }); move |greedy: &mut GreedyMesh< FigureSpriteAtlasData, SpriteAtlasAllocator, >, sprite_mesh: &mut Mesh| { prof_span!("mesh sprite"); let lod_sprite_data = scaled.map(|lod_scale_orig| { let lod_scale = model_scale * if lod_scale_orig == 1.0 { Vec3::broadcast(1.0) } else { lod_axes * lod_scale_orig + lod_axes.map(|e| if e == 0.0 { 1.0 } else { 0.0 }) }; // Get starting page count of opaque mesh let start_page_num = sprite_mesh.vertices().len() / SPRITE_VERT_PAGE_SIZE as usize; // Mesh generation exclusively acts using side effects; it // has no interesting return value, but updates the mesh. generate_mesh_base_vol_sprite( Segment::from_vox_model_index(&model.read().0, 0) .scaled_by(lod_scale), (greedy, sprite_mesh, false), offset.map(|e: f32| e.floor()) * lod_scale, ); // Get the number of pages after the model was meshed let end_page_num = (sprite_mesh.vertices().len() + SPRITE_VERT_PAGE_SIZE as usize - 1) / SPRITE_VERT_PAGE_SIZE as usize; // Fill the current last page up with degenerate verts sprite_mesh.vertices_mut_vec().resize_with( end_page_num * SPRITE_VERT_PAGE_SIZE as usize, SpriteVertex::default, ); let sprite_scale = Vec3::one() / lod_scale; SpriteData { vert_pages: start_page_num as u32..end_page_num as u32, scale: sprite_scale, offset: offset.map(|e| e.rem_euclid(1.0)), } }); ((kind, variation, filter_variant), lod_sprite_data) } }, ) }) .map(|f| f(&mut greedy, &mut sprite_mesh)) .collect(); let (sprite_atlas_texture_data, sprite_atlas_size) = { prof_span!("finalize"); greedy.finalize() }; SpriteWorkerResponse { sprite_config, sprite_data, sprite_atlas_texture_data, sprite_atlas_size, sprite_mesh, } }); let init = core::cell::OnceCell::new(); let mut join_handle = Some(join_handle); let mut closure = move |renderer: &mut Renderer| { // The second unwrap can only fail if the sprite meshing thread panics, which // implies that our sprite assets either were not found or did not // satisfy the size requirements for meshing, both of which are // considered invariant violations. let SpriteWorkerResponse { sprite_config, sprite_data, sprite_atlas_texture_data, sprite_atlas_size, sprite_mesh, } = join_handle .take() .expect( "Closure should only be called once (in a `OnceCell::get_or_init`) in the \ absence of caught panics!", ) .join() .unwrap(); let [sprite_col_lights] = sprite_atlas_texture_data.create_textures(renderer, sprite_atlas_size); let sprite_atlas_textures = renderer.sprite_bind_atlas_textures(sprite_col_lights); // Write sprite model to a 1D texture let sprite_verts_buffer = renderer.create_sprite_verts(sprite_mesh); Self { state: SpriteRenderState { // TODO: these are all Arcs, would it makes sense to factor out the Arc? sprite_config: Arc::clone(&sprite_config), sprite_data: Arc::new(sprite_data), sprite_atlas_textures: Arc::new(sprite_atlas_textures), }, sprite_verts_buffer: Arc::new(sprite_verts_buffer), } }; Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone()) } } impl Terrain { pub fn new( renderer: &mut Renderer, global_model: &GlobalModel, lod_data: &LodData, sprite_render_context: SpriteRenderContext, ) -> Self { // Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating // with worker threads that are meshing chunks. let (send, recv) = channel::unbounded(); let (atlas, atlas_textures) = Self::make_atlas(renderer).expect("Failed to create atlas texture"); Self { atlas, chunks: HashMap::default(), shadow_chunks: Vec::default(), mesh_send_tmp: send, mesh_recv: recv, mesh_todo: HashMap::default(), mesh_todos_active: Arc::new(AtomicU64::new(0)), mesh_recv_overflow: 0.0, sprite_render_state: sprite_render_context.state, sprite_globals: renderer.bind_sprite_globals( global_model, lod_data, &sprite_render_context.sprite_verts_buffer, ), atlas_textures: Arc::new(atlas_textures), phantom: PhantomData, } } fn make_atlas( renderer: &mut Renderer, ) -> Result< ( AtlasAllocator, AtlasTextures, ), RenderError, > { span!(_guard, "make_atlas", "Terrain::make_atlas"); let max_texture_size = renderer.max_texture_size(); let atlas_size = guillotiere::Size::new(max_texture_size as i32, max_texture_size as i32); let atlas = AtlasAllocator::with_options(atlas_size, &guillotiere::AllocatorOptions { // TODO: Verify some good empirical constants. small_size_threshold: 128, large_size_threshold: 1024, ..guillotiere::AllocatorOptions::default() }); let [col_lights, kinds] = [wgpu::TextureFormat::Rgba8Unorm, wgpu::TextureFormat::R8Uint] .map(|fmt| { renderer.create_texture_raw( &wgpu::TextureDescriptor { label: Some("Terrain atlas texture"), size: wgpu::Extent3d { width: max_texture_size, height: max_texture_size, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: fmt, usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING, view_formats: &[], }, &wgpu::TextureViewDescriptor { label: Some("Terrain atlas texture view"), format: Some(fmt), dimension: Some(wgpu::TextureViewDimension::D2), aspect: wgpu::TextureAspect::All, base_mip_level: 0, mip_level_count: None, base_array_layer: 0, array_layer_count: None, }, &wgpu::SamplerDescriptor { label: Some("Terrain atlas sampler"), address_mode_u: wgpu::AddressMode::ClampToEdge, address_mode_v: wgpu::AddressMode::ClampToEdge, address_mode_w: wgpu::AddressMode::ClampToEdge, mag_filter: wgpu::FilterMode::Nearest, min_filter: wgpu::FilterMode::Nearest, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }, ) }); let textures = renderer.terrain_bind_atlas_textures(col_lights, kinds); Ok((atlas, textures)) } fn remove_chunk_meta(&mut self, _pos: Vec2, chunk: &TerrainChunkData) { // No need to free the allocation if the chunk is not allocated in the current // atlas, since we don't bother tracking it at that point. if let Some(atlas_alloc) = chunk.atlas_alloc { self.atlas.deallocate(atlas_alloc); } /* let (zmin, zmax) = chunk.z_bounds; self.z_index_up.remove(Vec3::from(zmin, pos.x, pos.y)); self.z_index_down.remove(Vec3::from(zmax, pos.x, pos.y)); */ } fn insert_chunk(&mut self, pos: Vec2, chunk: TerrainChunkData) { if let Some(old) = self.chunks.insert(pos, chunk) { self.remove_chunk_meta(pos, &old); } /* let (zmin, zmax) = chunk.z_bounds; self.z_index_up.insert(Vec3::from(zmin, pos.x, pos.y)); self.z_index_down.insert(Vec3::from(zmax, pos.x, pos.y)); */ } fn remove_chunk(&mut self, pos: Vec2) { if let Some(chunk) = self.chunks.remove(&pos) { self.remove_chunk_meta(pos, &chunk); // Temporarily remember dead chunks for shadowing purposes. self.shadow_chunks.push((pos, chunk)); } if let Some(_todo) = self.mesh_todo.remove(&pos) { //Do nothing on todo mesh removal. } } /// Find the light level (sunlight) at the given world position. pub fn light_at_wpos(&self, wpos: Vec3) -> f32 { let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| { e.div_euclid(sz as i32) }); self.chunks .get(&chunk_pos) .map(|c| (c.light_map)(wpos)) .unwrap_or(1.0) } /// Determine whether a given block change actually require remeshing. /// /// Returns (skip_color, skip_lights) where /// /// skip_color means no textures were recolored (i.e. this was a sprite only /// change). /// /// skip_lights means no remeshing or relighting was required /// (i.e. the block opacity / lighting info / block kind didn't change). fn skip_remesh(old_block: Block, new_block: Block) -> (bool, bool) { let same_mesh = // Both blocks are of the same opacity and same liquidity (since these are what we use // to determine mesh boundaries). new_block.is_liquid() == old_block.is_liquid() && new_block.is_opaque() == old_block.is_opaque(); let skip_lights = same_mesh && // Block glow and sunlight handling are the same (so we don't have to redo // lighting). new_block.get_glow() == old_block.get_glow() && new_block.get_max_sunlight() == old_block.get_max_sunlight(); let skip_color = same_mesh && // Both blocks are uncolored !new_block.has_color() && !old_block.has_color(); (skip_color, skip_lights) } /// Find the glow level (light from lamps) at the given world position. pub fn glow_at_wpos(&self, wpos: Vec3) -> f32 { let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| { e.div_euclid(sz as i32) }); self.chunks .get(&chunk_pos) .map(|c| (c.glow_map)(wpos)) .unwrap_or(0.0) } pub fn glow_normal_at_wpos(&self, wpos: Vec3) -> (Vec3, f32) { let wpos_chunk = wpos.xy().map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); const AMBIANCE: f32 = 0.15; // 0-1, the proportion of light that should illuminate the rear of an object let (bias, total) = Spiral2d::new() .take(9) .flat_map(|rpos| { let chunk_pos = wpos_chunk + rpos; self.chunks .get(&chunk_pos) .into_iter() .flat_map(|c| c.blocks_of_interest.lights.iter()) .filter_map(move |(lpos, level)| { if (*lpos - wpos_chunk).map(|e| e.abs()).reduce_min() < SUNLIGHT as i32 + 2 { Some(( Vec3::::from( chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32), ) + *lpos, level, )) } else { None } }) }) .fold( (Vec3::broadcast(0.001), 0.0), |(bias, total), (lpos, level)| { let rpos = lpos.map(|e| e as f32 + 0.5) - wpos; let level = (*level as f32 - rpos.magnitude()).max(0.0) * SUNLIGHT_INV; ( bias + rpos.try_normalized().unwrap_or_else(Vec3::zero) * level, total + level, ) }, ); let bias_factor = bias.magnitude() * (1.0 - AMBIANCE) / total.max(0.001); ( bias.try_normalized().unwrap_or_else(Vec3::zero) * bias_factor.powf(0.5), self.glow_at_wpos(wpos.map(|e| e.floor() as i32)), ) } /// Maintain terrain data. To be called once per tick. /// /// The returned visible bounding volumes take into account the current /// camera position (i.e: when underground, surface structures will be /// culled from the volume). pub fn maintain( &mut self, renderer: &mut Renderer, scene_data: &SceneData, focus_pos: Vec3, loaded_distance: f32, camera: &Camera, ) -> ( Aabb, Vec>, math::Aabr, Vec>, math::Aabr, ) { let camera::Dependents { view_mat, proj_mat_treeculler, cam_pos, .. } = camera.dependents(); // Remove any models for chunks that have been recently removed. // Note: Does this before adding to todo list just in case removed chunks were // replaced with new chunks (although this would probably be recorded as // modified chunks) for &pos in &scene_data.state.terrain_changes().removed_chunks { self.remove_chunk(pos); // Remove neighbors from meshing todo for i in -1..2 { for j in -1..2 { if i != 0 || j != 0 { self.mesh_todo.remove(&(pos + Vec2::new(i, j))); } } } } span!(_guard, "maintain", "Terrain::maintain"); let current_tick = scene_data.tick; let current_time = scene_data.state.get_time(); // The visible bounding box of all chunks, not including culled regions let mut visible_bounding_box: Option> = None; // Add any recently created or changed chunks to the list of chunks to be // meshed. span!(guard, "Add new/modified chunks to mesh todo list"); for (modified, pos) in scene_data .state .terrain_changes() .modified_chunks .iter() .map(|c| (true, c)) .chain( scene_data .state .terrain_changes() .new_chunks .iter() .map(|c| (false, c)), ) { // TODO: ANOTHER PROBLEM HERE! // What happens if the block on the edge of a chunk gets modified? We need to // spawn a mesh worker to remesh its neighbour(s) too since their // ambient occlusion and face elision information changes too! for i in -1..2 { for j in -1..2 { let pos = pos + Vec2::new(i, j); if !(self.chunks.contains_key(&pos) || self.mesh_todo.contains_key(&pos)) || modified { let mut neighbours = true; for i in -1..2 { for j in -1..2 { neighbours &= scene_data .state .terrain() .contains_key_real(pos + Vec2::new(i, j)); } } if neighbours { self.mesh_todo.insert(pos, ChunkMeshState { pos, started_tick: current_tick, is_worker_active: false, skip_remesh: false, }); } } } } } drop(guard); // Add the chunks belonging to recently changed blocks to the list of chunks to // be meshed span!(guard, "Add chunks with modified blocks to mesh todo list"); // TODO: would be useful if modified blocks were grouped by chunk for (&pos, &old_block) in scene_data.state.terrain_changes().modified_blocks.iter() { // terrain_changes() are both set and applied during the same tick on the // client, so the current state is the new state and modified_blocks // stores the old state. let new_block = scene_data.state.get_block(pos); let (skip_color, skip_lights) = if let Some(new_block) = new_block { Self::skip_remesh(old_block, new_block) } else { // The block coordinates of a modified block should be in bounds, since they are // only retained if setting the block was successful during the state tick in // client. So this is definitely a bug, but we can recover safely by just // conservatively doing a full remesh in this case, rather than crashing the // game. warn!( "Invariant violation: pos={:?} should be a valid block position. This is a \ bug; please contact the developers if you see this error message!", pos ); (false, false) }; // Currently, we can only skip remeshing if both lights and // colors don't need to be reworked. let skip_remesh = skip_color && skip_lights; // TODO: Be cleverer about this to avoid remeshing all neighbours. There are a // few things that can create an 'effect at a distance'. These are // as follows: // - A glowing block is added or removed, thereby causing a lighting // recalculation proportional to its glow radius. // - An opaque block that was blocking sunlight from entering a cavity is // removed (or added) thereby // changing the way that sunlight propagates into the cavity. // // We can and should be cleverer about this, but it's non-trivial. For now, we // don't remesh if only a block color changed or a sprite was // altered in a way that doesn't affect its glow, but we make no // attempt to do smarter cavity checking (to see if altering the // block changed the sunlight neighbors could get). // let block_effect_radius = block.get_glow().unwrap_or(0).max(1); let block_effect_radius = crate::mesh::terrain::MAX_LIGHT_DIST; // Handle block changes on chunk borders // Remesh all neighbours because we have complex lighting now // TODO: if lighting is on the server this can be updated to only remesh when // lighting changes in that neighbouring chunk or if the block // change was on the border for x in -1..2 { for y in -1..2 { let neighbour_pos = pos + Vec3::new(x, y, 0) * block_effect_radius; let neighbour_chunk_pos = scene_data.state.terrain().pos_key(neighbour_pos); if skip_lights && !(x == 0 && y == 0) { // We don't need to remesh neighboring chunks if this block change doesn't // require relighting. continue; } // Only remesh if this chunk has all its neighbors let mut neighbours = true; for i in -1..2 { for j in -1..2 { neighbours &= scene_data .state .terrain() .contains_key_real(neighbour_chunk_pos + Vec2::new(i, j)); } } if neighbours { let todo = self.mesh_todo .entry(neighbour_chunk_pos) .or_insert(ChunkMeshState { pos: neighbour_chunk_pos, started_tick: current_tick, is_worker_active: false, skip_remesh, }); // Make sure not to skip remeshing a chunk if it already had to be // fully meshed for other reasons. Even if the mesh is currently active // (so relighting would be redundant), we currently have to remesh // everything unless the previous mesh was also able to skip remeshing, // since otherwise the active remesh is computing new lighting values // that we don't have yet. todo.skip_remesh &= skip_remesh; todo.is_worker_active = false; todo.started_tick = current_tick; } } } } drop(guard); // Limit ourselves to u16::MAX even if larger textures are supported. let max_texture_size = renderer.max_texture_size(); let meshing_cores = match num_cpus::get() as u64 { n if n < 4 => 1, n if n < 8 => n - 3, n => n - 4, }; span!(guard, "Queue meshing from todo list"); let mesh_focus_pos = focus_pos.map(|e| e.trunc()).xy().as_::(); //TODO: this is actually no loop, it just runs for a single entry because of // the `min_by_key`. Evaluate actually looping here while let Some((todo, chunk)) = self .mesh_todo .values_mut() .filter(|todo| !todo.is_worker_active) .min_by_key(|todo| ((todo.pos.as_::() * TerrainChunk::RECT_SIZE.as_::()).distance_squared(mesh_focus_pos), 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() .or_else(|| { warn!("Invariant violation: a chunk whose neighbors have not been fetched was found in the todo list, which could halt meshing entirely."); None })?)) }) { if self.mesh_todos_active.load(Ordering::Relaxed) > meshing_cores { break; } // like ambient occlusion and edge elision, we also need the borders // of the chunk's neighbours too (hence the `- 1` and `+ 1`). let aabr = Aabr { min: todo .pos .map2(VolGrid2d::::chunk_size(), |e, sz| e * sz as i32 - 1), max: todo.pos.map2(VolGrid2d::::chunk_size(), |e, sz| { (e + 1) * sz as i32 + 1 }), }; // Copy out the chunk data we need to perform the meshing. We do this by taking // a sample of the terrain that includes both the chunk we want and // its neighbours. let volume = match scene_data.state.terrain().sample(aabr) { Ok(sample) => sample, /* TODO: Ensure that all of the chunk's neighbours still * exist to avoid buggy shadow borders */ // Either this chunk or its neighbours doesn't yet exist, so we keep it in the // queue to be processed at a later date when we have its neighbours. Err(VolGrid2dError::NoSuchChunk) => { continue; }, _ => panic!("Unhandled edge case"), }; // The region to actually mesh let min_z = volume .iter() .fold(i32::MAX, |min, (_, chunk)| chunk.get_min_z().min(min)); let max_z = volume .iter() .fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max)); let aabb = Aabb { min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 2), max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 2), }; // Clone various things so that they can be moved into the thread. let send = self.mesh_send_tmp.clone(); let pos = todo.pos; let chunks = &self.chunks; let skip_remesh = todo .skip_remesh .then_some(()) .and_then(|_| chunks.get(&pos)) .map(|chunk| (Arc::clone(&chunk.light_map), Arc::clone(&chunk.glow_map))); // Queue the worker thread. let started_tick = todo.started_tick; let sprite_data = Arc::clone(&self.sprite_render_state.sprite_data); let sprite_config = Arc::clone(&self.sprite_render_state.sprite_config); let cnt = Arc::clone(&self.mesh_todos_active); cnt.fetch_add(1, Ordering::Relaxed); scene_data .state .slow_job_pool() .spawn("TERRAIN_MESHING", move || { let sprite_data = sprite_data; let _ = send.send(mesh_worker( pos, (min_z as f32, max_z as f32), skip_remesh, started_tick, volume, max_texture_size as u16, chunk, aabb, &sprite_data, &sprite_config, )); cnt.fetch_sub(1, Ordering::Relaxed); }); todo.is_worker_active = true; } drop(guard); // Receive a chunk mesh from a worker thread and upload it to the GPU, then // store it. Vary the rate at which we pull items out to correlate with the // framerate, preventing tail latency. span!(guard, "Get/upload meshed chunk"); const CHUNKS_PER_SECOND: f32 = 240.0; let recv_count = scene_data.state.get_delta_time() * CHUNKS_PER_SECOND + self.mesh_recv_overflow; self.mesh_recv_overflow = recv_count.fract(); let incoming_chunks = std::iter::from_fn(|| self.mesh_recv.recv_timeout(Duration::new(0, 0)).ok()) .take(recv_count.floor() as usize) .collect::>(); // Avoid ownership issue for response in incoming_chunks { match self.mesh_todo.get(&response.pos) { // It's the mesh we want, insert the newly finished model into the terrain model // data structure (convert the mesh to a model first of course). Some(todo) if response.started_tick <= todo.started_tick => { let started_tick = todo.started_tick; let sprite_instances = response.sprite_instances.map(|(instances, alt_indices)| { (renderer.create_instances(&instances), alt_indices) }); if let Some(mesh) = response.mesh { // Full update, insert the whole chunk. let load_time = self .chunks .get(&response.pos) .map(|chunk| chunk.load_time) .unwrap_or(current_time as f32); // TODO: Allocate new atlas on allocation failure. let atlas = &mut self.atlas; let chunks = &mut self.chunks; let atlas_textures = &mut self.atlas_textures; let alloc_size = guillotiere::Size::new( i32::from(mesh.atlas_size.x), i32::from(mesh.atlas_size.y), ); let allocation = atlas.allocate(alloc_size).unwrap_or_else(|| { // Atlas allocation failure: try allocating a new texture and atlas. let (new_atlas, new_atlas_textures) = Self::make_atlas(renderer).expect("Failed to create atlas texture"); // We reset the atlas and clear allocations from existing chunks, // even though we haven't yet // checked whether the new allocation can fit in // the texture. This is reasonable because we don't have a fallback // if a single chunk can't fit in an empty atlas of maximum size. // // TODO: Consider attempting defragmentation first rather than just // always moving everything into the new chunk. chunks.iter_mut().for_each(|(_, chunk)| { chunk.atlas_alloc = None; }); *atlas = new_atlas; *atlas_textures = Arc::new(new_atlas_textures); atlas .allocate(alloc_size) .expect("Chunk data does not fit in a texture of maximum size.") }); // NOTE: Cast is safe since the origin was a u16. let atlas_offs = Vec2::new( allocation.rectangle.min.x as u32, allocation.rectangle.min.y as u32, ); // Update col_lights texture renderer.update_texture( &atlas_textures.textures[0], atlas_offs.into_array(), mesh.atlas_size.as_().into_array(), &mesh.atlas_texture_data.col_lights, ); // Update kinds texture renderer.update_texture( &atlas_textures.textures[1], atlas_offs.into_array(), mesh.atlas_size.as_().into_array(), &mesh.atlas_texture_data.kinds, ); self.insert_chunk(response.pos, TerrainChunkData { load_time, opaque_model: renderer.create_model(&mesh.opaque_mesh), fluid_model: renderer.create_model(&mesh.fluid_mesh), atlas_alloc: Some(allocation.id), atlas_textures: Arc::clone(&self.atlas_textures), light_map: mesh.light_map, glow_map: mesh.glow_map, sprite_instances, locals: renderer.create_terrain_bound_locals(&[TerrainLocals::new( Vec3::from( response.pos.map2(VolGrid2d::::chunk_size(), |e, sz| { e as f32 * sz as f32 }), ), Quaternion::identity(), atlas_offs, load_time, )]), visible: Visibility { in_range: false, in_frustum: false, }, can_shadow_point: false, can_shadow_sun: false, blocks_of_interest: response.blocks_of_interest, z_bounds: mesh.z_bounds, sun_occluder_z_bounds: mesh.sun_occluder_z_bounds, frustum_last_plane_index: 0, alt_indices: mesh.alt_indices, }); } else if let Some(chunk) = self.chunks.get_mut(&response.pos) { // There was an update that didn't require a remesh (probably related to // non-glowing sprites) so we just update those. chunk.sprite_instances = sprite_instances; chunk.blocks_of_interest = response.blocks_of_interest; } if response.started_tick == started_tick { self.mesh_todo.remove(&response.pos); } }, // Chunk must have been removed, or it was spawned on an old tick. Drop the mesh // since it's either out of date or no longer needed. Some(_todo) => {}, None => {}, } } drop(guard); // Construct view frustum span!(guard, "Construct view frustum"); let focus_off = focus_pos.map(|e| e.trunc()); let frustum = Frustum::from_modelview_projection( (proj_mat_treeculler * view_mat * Mat4::translation_3d(-focus_off)).into_col_arrays(), ); drop(guard); // Update chunk visibility span!(guard, "Update chunk visibility"); let chunk_sz = V::RECT_SIZE.x as f32; for (pos, chunk) in &mut self.chunks { let chunk_pos = pos.as_::() * chunk_sz; chunk.can_shadow_sun = false; // Limit focus_pos to chunk bounds and ensure the chunk is within the fog // boundary let nearest_in_chunk = Vec2::from(focus_pos).clamped(chunk_pos, chunk_pos + chunk_sz); let distance_2 = Vec2::::from(focus_pos).distance_squared(nearest_in_chunk); let in_range = distance_2 < loaded_distance.powi(2); chunk.visible.in_range = in_range; // Ensure the chunk is within the view frustum let chunk_min = [chunk_pos.x, chunk_pos.y, chunk.z_bounds.0]; let chunk_max = [ chunk_pos.x + chunk_sz, chunk_pos.y + chunk_sz, chunk.sun_occluder_z_bounds.1, ]; let (in_frustum, last_plane_index) = AABB::new(chunk_min, chunk_max) .coherent_test_against_frustum(&frustum, chunk.frustum_last_plane_index); chunk.frustum_last_plane_index = last_plane_index; chunk.visible.in_frustum = in_frustum; let chunk_area = Aabr { min: chunk_pos, max: chunk_pos + chunk_sz, }; if in_frustum { let visible_box = Aabb { min: chunk_area.min.with_z(chunk.sun_occluder_z_bounds.0), max: chunk_area.max.with_z(chunk.sun_occluder_z_bounds.1), }; visible_bounding_box = visible_bounding_box .map(|e| e.union(visible_box)) .or(Some(visible_box)); } // FIXME: Hack that only works when only the lantern casts point shadows // (and hardcodes the shadow distance). Should ideally exist per-light, too. chunk.can_shadow_point = distance_2 < (128.0 * 128.0); } drop(guard); span!(guard, "Shadow magic"); // PSRs: potential shadow receivers let visible_bounding_box = visible_bounding_box.unwrap_or(Aabb { min: focus_pos - 2.0, max: focus_pos + 2.0, }); let inv_proj_view = math::Mat4::from_col_arrays((proj_mat_treeculler * view_mat).into_col_arrays()) .as_::() .inverted(); // PSCs: Potential shadow casters let ray_direction = scene_data.get_sun_dir(); let collides_with_aabr = |a: math::Aabb, b: math::Aabr| { let min = math::Vec4::new(a.min.x, a.min.y, b.min.x, b.min.y); let max = math::Vec4::new(b.max.x, b.max.y, a.max.x, a.max.y); #[cfg(feature = "simd")] return min.partial_cmple_simd(max).reduce_and(); #[cfg(not(feature = "simd"))] return min.partial_cmple(&max).reduce_and(); }; let (visible_light_volume, visible_psr_bounds) = if ray_direction.z < 0.0 && renderer.pipeline_modes().shadow.is_map() { let visible_bounding_box = math::Aabb:: { min: math::Vec3::from(visible_bounding_box.min - focus_off), max: math::Vec3::from(visible_bounding_box.max - focus_off), }; let focus_off = math::Vec3::from(focus_off); let visible_bounds_fine = visible_bounding_box.as_::(); let ray_direction = math::Vec3::::from(ray_direction); // NOTE: We use proj_mat_treeculler here because // calc_focused_light_volume_points makes the assumption that the // near plane lies before the far plane. let visible_light_volume = math::calc_focused_light_volume_points( inv_proj_view, ray_direction.as_::(), visible_bounds_fine, 1e-6, ) .map(|v| v.as_::()) .collect::>(); let up: math::Vec3 = { math::Vec3::unit_y() }; let cam_pos = math::Vec3::from(cam_pos); let ray_mat = math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, up); let visible_bounds = math::Aabr::from(math::fit_psr( ray_mat, visible_light_volume.iter().copied(), |p| p, )); let ray_mat = ray_mat * math::Mat4::translation_3d(-focus_off); let can_shadow_sun = |pos: Vec2, chunk: &TerrainChunkData| { let chunk_pos = pos.as_::() * chunk_sz; // Ensure the chunk is within the PSR set. let chunk_box = math::Aabb { min: math::Vec3::new(chunk_pos.x, chunk_pos.y, chunk.z_bounds.0), max: math::Vec3::new( chunk_pos.x + chunk_sz, chunk_pos.y + chunk_sz, chunk.z_bounds.1, ), }; let chunk_from_light = math::fit_psr( ray_mat, math::aabb_to_points(chunk_box).iter().copied(), |p| p, ); collides_with_aabr(chunk_from_light, visible_bounds) }; // Handle potential shadow casters (chunks that aren't visible, but are still in // range) to see if they could cast shadows. self.chunks.iter_mut() // NOTE: We deliberately avoid doing this computation for chunks we already know // are visible, since by definition they'll always intersect the visible view // frustum. .filter(|chunk| !chunk.1.visible.in_frustum) .for_each(|(&pos, chunk)| { chunk.can_shadow_sun = can_shadow_sun(pos, chunk); }); // Handle dead chunks that we kept around only to make sure shadows don't blink // out when a chunk disappears. // // If the sun can currently cast shadows, we retain only those shadow chunks // that both: 1. have not been replaced by a real chunk instance, // and 2. are currently potential shadow casters (as witnessed by // `can_shadow_sun` returning true). // // NOTE: Please make sure this runs *after* any code that could insert a chunk! // Otherwise we may end up with multiple instances of the chunk trying to cast // shadows at the same time. let chunks = &self.chunks; self.shadow_chunks .retain(|(pos, chunk)| !chunks.contains_key(pos) && can_shadow_sun(*pos, chunk)); (visible_light_volume, visible_bounds) } else { // There's no daylight or no shadows, so there's no reason to keep any // shadow chunks around. self.shadow_chunks.clear(); (Vec::new(), math::Aabr { min: math::Vec2::zero(), max: math::Vec2::zero(), }) }; drop(guard); span!(guard, "Rain occlusion magic"); // Check if there is rain near the camera let max_weather = scene_data .state .max_weather_near(focus_off.xy() + cam_pos.xy()); let (visible_occlusion_volume, visible_por_bounds) = if max_weather.rain > RAIN_THRESHOLD { let visible_bounding_box = math::Aabb:: { min: math::Vec3::from(visible_bounding_box.min - focus_off), max: math::Vec3::from(visible_bounding_box.max - focus_off), }; let visible_bounds_fine = math::Aabb { min: visible_bounding_box.min.as_::(), max: visible_bounding_box.max.as_::(), }; let weather = scene_data.client.weather_at_player(); let ray_direction = math::Vec3::::from(weather.rain_vel().normalized()); // NOTE: We use proj_mat_treeculler here because // calc_focused_light_volume_points makes the assumption that the // near plane lies before the far plane. let visible_volume = math::calc_focused_light_volume_points( inv_proj_view, ray_direction.as_::(), visible_bounds_fine, 1e-6, ) .map(|v| v.as_::()) .collect::>(); let cam_pos = math::Vec3::from(cam_pos); let ray_mat = math::Mat4::look_at_rh(cam_pos, cam_pos + ray_direction, math::Vec3::unit_y()); let visible_bounds = math::Aabr::from(math::fit_psr( ray_mat, visible_volume.iter().copied(), |p| p, )); (visible_volume, visible_bounds) } else { (Vec::new(), math::Aabr::default()) }; drop(guard); ( visible_bounding_box, visible_light_volume, visible_psr_bounds, visible_occlusion_volume, visible_por_bounds, ) } 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 { self.chunks .iter() .filter(|(_, c)| c.visible.is_visible()) .count() } pub fn shadow_chunk_count(&self) -> usize { self.shadow_chunks.len() } pub fn render_shadows<'a>( &'a self, drawer: &mut TerrainShadowDrawer<'_, 'a>, focus_pos: Vec3, culling_mode: CullingMode, ) { span!(_guard, "render_shadows", "Terrain::render_shadows"); let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); let chunk_iter = Spiral2d::new() .filter_map(|rpos| { let pos = focus_chunk + rpos; self.chunks.get(&pos) }) .take(self.chunks.len()); // Directed shadows // // NOTE: We also render shadows for dead chunks that were found to still be // potential shadow casters, to avoid shadows suddenly disappearing at // very steep sun angles (e.g. sunrise / sunset). chunk_iter .filter(|chunk| chunk.can_shadow_sun()) .chain(self.shadow_chunks.iter().map(|(_, chunk)| chunk)) .filter_map(|chunk| { Some(( chunk.opaque_model.as_ref()?, &chunk.locals, &chunk.alt_indices, )) }) .for_each(|(model, locals, alt_indices)| { drawer.draw(model, locals, alt_indices, culling_mode) }); } pub fn render_rain_occlusion<'a>( &'a self, drawer: &mut TerrainShadowDrawer<'_, 'a>, focus_pos: Vec3, ) { span!(_guard, "render_occlusion", "Terrain::render_occlusion"); let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); let chunk_iter = Spiral2d::new() .filter_map(|rpos| { let pos = focus_chunk + rpos; self.chunks.get(&pos) }) .take(self.chunks.len().min(RAIN_OCCLUSION_CHUNKS)); chunk_iter // Find a way to keep this? // .filter(|chunk| chunk.can_shadow_sun()) .filter_map(|chunk| Some(( chunk .opaque_model .as_ref()?, &chunk.locals, &chunk.alt_indices, ))) .for_each(|(model, locals, alt_indices)| drawer.draw(model, locals, alt_indices, CullingMode::None)); } pub fn chunks_for_point_shadows( &self, focus_pos: Vec3, ) -> impl Clone + Iterator< Item = ( &Model, &pipelines::terrain::BoundLocals, ), > { let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); let chunk_iter = Spiral2d::new() .filter_map(move |rpos| { let pos = focus_chunk + rpos; self.chunks.get(&pos) }) .take(self.chunks.len()); // Point shadows // // NOTE: We don't bother retaining chunks unless they cast sun shadows, so we // don't use `shadow_chunks` here. chunk_iter .filter(|chunk| chunk.can_shadow_point) .filter_map(|chunk| { chunk .opaque_model .as_ref() .map(|model| (model, &chunk.locals)) }) } pub fn render<'a>( &'a self, drawer: &mut FirstPassDrawer<'a>, focus_pos: Vec3, culling_mode: CullingMode, ) { span!(_guard, "render", "Terrain::render"); let mut drawer = drawer.draw_terrain(); let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); Spiral2d::new() .filter_map(|rpos| { let pos = focus_chunk + rpos; Some((rpos, self.chunks.get(&pos)?)) }) .take(self.chunks.len()) .filter(|(_, chunk)| chunk.visible.is_visible()) .filter_map(|(rpos, chunk)| { Some(( rpos, chunk.opaque_model.as_ref()?, &chunk.atlas_textures, &chunk.locals, &chunk.alt_indices, )) }) .for_each(|(rpos, model, atlas_textures, locals, alt_indices)| { // Always draw all of close chunks to avoid terrain 'popping' let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) { CullingMode::None } else { culling_mode }; drawer.draw(model, atlas_textures, locals, alt_indices, culling_mode) }); } pub fn render_sprites<'a>( &'a self, sprite_drawer: &mut SpriteDrawer<'_, 'a>, focus_pos: Vec3, cam_pos: Vec3, sprite_render_distance: f32, culling_mode: CullingMode, ) { span!(_guard, "render_sprites", "Terrain::render_sprites"); let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); // Avoid switching textures let chunk_iter = Spiral2d::new() .filter_map(|rpos| { let pos = focus_chunk + rpos; Some((rpos, pos, self.chunks.get(&pos)?)) }) .take(self.chunks.len()); let chunk_size = V::RECT_SIZE.map(|e| e as f32); let sprite_low_detail_distance = sprite_render_distance * 0.75; let sprite_mid_detail_distance = sprite_render_distance * 0.5; let sprite_hid_detail_distance = sprite_render_distance * 0.35; let sprite_high_detail_distance = sprite_render_distance * 0.15; chunk_iter .clone() .filter(|(_, _, c)| c.visible.is_visible()) .for_each(|(rpos, pos, chunk)| { // Skip chunk if it has no sprites if chunk.sprite_instances[0].0.count() == 0 { return; } let chunk_center = pos.map2(chunk_size, |e, sz| (e as f32 + 0.5) * sz); let focus_dist_sqrd = Vec2::from(focus_pos).distance_squared(chunk_center); let dist_sqrd = Aabr { min: chunk_center - chunk_size * 0.5, max: chunk_center + chunk_size * 0.5, } .projected_point(cam_pos.xy()) .distance_squared(cam_pos.xy()); if focus_dist_sqrd < sprite_render_distance.powi(2) { let lod_level = if dist_sqrd < sprite_high_detail_distance.powi(2) { 0 } else if dist_sqrd < sprite_hid_detail_distance.powi(2) { 1 } else if dist_sqrd < sprite_mid_detail_distance.powi(2) { 2 } else if dist_sqrd < sprite_low_detail_distance.powi(2) { 3 } else { 4 }; // Always draw all of close chunks to avoid terrain 'popping' let culling_mode = if rpos.magnitude_squared() < NEVER_CULL_DIST.pow(2) { CullingMode::None } else { culling_mode }; sprite_drawer.draw( &chunk.locals, &chunk.sprite_instances[lod_level].0, &chunk.sprite_instances[lod_level].1, culling_mode, ); } }); } pub fn render_translucent<'a>( &'a self, drawer: &mut FirstPassDrawer<'a>, focus_pos: Vec3, ) { span!(_guard, "render_translucent", "Terrain::render_translucent"); let focus_chunk = Vec2::from(focus_pos).map2(TerrainChunk::RECT_SIZE, |e: f32, sz| { (e as i32).div_euclid(sz as i32) }); // Avoid switching textures let chunk_iter = Spiral2d::new() .filter_map(|rpos| { let pos = focus_chunk + rpos; self.chunks.get(&pos).map(|c| (pos, c)) }) .take(self.chunks.len()); // Translucent span!(guard, "Fluid chunks"); let mut fluid_drawer = drawer.draw_fluid(); chunk_iter .filter(|(_, chunk)| chunk.visible.is_visible()) .filter_map(|(_, chunk)| { chunk .fluid_model .as_ref() .map(|model| (model, &chunk.locals)) }) .collect::>() .into_iter() .rev() // Render back-to-front .for_each(|(model, locals)| { fluid_drawer.draw( model, locals, ) }); drop(fluid_drawer); drop(guard); } }