From 3869cdf1d2949a745688fa7ca6015a4e34a72d34 Mon Sep 17 00:00:00 2001 From: Joshua Yanovski Date: Tue, 11 May 2021 10:53:10 -0700 Subject: [PATCH] Don't remesh chunk vertex data on sprite update. This results in an extremely visually noticeable improvement in latency when adding or removing sprite data and makes the game feel more responsive. This happens, for instance, when picking up a sprite like an apple or flower from the environment. We check to make sure that for items with lighting (like Velorite) or changes that otherwise affect meshing (like changing from fluid to nonfluid) this doesn't trigger. --- common/src/grid.rs | 4 +- common/src/terrain/chonk.rs | 6 +- common/src/vol.rs | 4 +- common/src/volumes/chunk.rs | 10 +- common/src/volumes/dyna.rs | 4 +- common/src/volumes/vol_grid_2d.rs | 2 +- common/src/volumes/vol_grid_3d.rs | 2 +- common/state/src/state.rs | 26 ++- server/src/lib.rs | 2 + voxygen/src/mesh/terrain.rs | 10 +- voxygen/src/scene/terrain.rs | 370 +++++++++++++++++++----------- 11 files changed, 281 insertions(+), 159 deletions(-) diff --git a/common/src/grid.rs b/common/src/grid.rs index 561ec6a746..625e5d8b76 100644 --- a/common/src/grid.rs +++ b/common/src/grid.rs @@ -53,9 +53,9 @@ impl Grid { self.cells.get_mut(idx) } - pub fn set(&mut self, pos: Vec2, cell: T) -> Option<()> { + pub fn set(&mut self, pos: Vec2, cell: T) -> Option { let idx = self.idx(pos)?; - self.cells.get_mut(idx).map(|c| *c = cell) + self.cells.get_mut(idx).map(|c| core::mem::replace(c, cell)) } pub fn iter(&self) -> impl Iterator, &T)> + '_ { diff --git a/common/src/terrain/chonk.rs b/common/src/terrain/chonk.rs index 9651d5ffde..29240560de 100644 --- a/common/src/terrain/chonk.rs +++ b/common/src/terrain/chonk.rs @@ -167,13 +167,13 @@ impl ReadVol for Chonk { impl WriteVol for Chonk { #[inline(always)] - fn set(&mut self, pos: Vec3, block: Self::Vox) -> Result<(), Self::Error> { + fn set(&mut self, pos: Vec3, block: Self::Vox) -> Result { let mut sub_chunk_idx = self.sub_chunk_idx(pos.z); if pos.z < self.get_min_z() { // Make sure we're not adding a redundant chunk. if block == self.below { - return Ok(()); + return Ok(self.below.clone()); } // Prepend exactly sufficiently many SubChunks via Vec::splice let c = Chunk::, M>::filled(self.below.clone(), self.meta.clone()); @@ -184,7 +184,7 @@ impl WriteVol for Chonk } else if pos.z >= self.get_max_z() { // Make sure we're not adding a redundant chunk. if block == self.above { - return Ok(()); + return Ok(self.above.clone()); } // Append exactly sufficiently many SubChunks via Vec::extend let c = Chunk::, M>::filled(self.above.clone(), self.meta.clone()); diff --git a/common/src/vol.rs b/common/src/vol.rs index a9a82727b1..6dbeb438cc 100644 --- a/common/src/vol.rs +++ b/common/src/vol.rs @@ -133,7 +133,7 @@ pub trait SampleVol: BaseVol { pub trait WriteVol: BaseVol { /// Set the voxel at the provided position in the volume to the provided /// value. - fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), Self::Error>; + fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result; /// Map a voxel to another using the provided function. // TODO: Is `map` the right name? Implies a change in type. @@ -141,7 +141,7 @@ pub trait WriteVol: BaseVol { &mut self, pos: Vec3, f: F, - ) -> Result<(), Self::Error> + ) -> Result where Self: ReadVol, Self::Vox: Clone, diff --git a/common/src/volumes/chunk.rs b/common/src/volumes/chunk.rs index d680957b5a..34883e77b2 100644 --- a/common/src/volumes/chunk.rs +++ b/common/src/volumes/chunk.rs @@ -271,15 +271,17 @@ impl Chunk { } #[inline(always)] - fn set_unchecked(&mut self, pos: Vec3, vox: V) + fn set_unchecked(&mut self, pos: Vec3, vox: V) -> V where V: Clone + PartialEq, { if vox != self.default { let idx = self.force_idx_unchecked(pos); - self.vox[idx] = vox; + core::mem::replace(&mut self.vox[idx], vox) } else if let Some(idx) = self.idx_unchecked(pos) { - self.vox[idx] = vox; + core::mem::replace(&mut self.vox[idx], vox) + } else { + self.default.clone() } } } @@ -310,7 +312,7 @@ impl ReadVol for Chunk { impl WriteVol for Chunk { #[inline(always)] #[allow(clippy::unit_arg)] // TODO: Pending review in #587 - fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), Self::Error> { + fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result { if !pos .map2(S::SIZE, |e, s| 0 <= e && e < s as i32) .reduce_and() diff --git a/common/src/volumes/dyna.rs b/common/src/volumes/dyna.rs index b6cb3e6bdc..9f38c189cd 100644 --- a/common/src/volumes/dyna.rs +++ b/common/src/volumes/dyna.rs @@ -93,10 +93,10 @@ impl ReadVol for Dyna { impl WriteVol for Dyna { #[inline(always)] - fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result<(), DynaError> { + fn set(&mut self, pos: Vec3, vox: Self::Vox) -> Result { Self::idx_for(self.sz, pos) .and_then(|idx| self.vox.get_mut(idx)) - .map(|old_vox| *old_vox = vox) + .map(|old_vox| core::mem::replace(old_vox, vox)) .ok_or(DynaError::OutOfBounds) } } diff --git a/common/src/volumes/vol_grid_2d.rs b/common/src/volumes/vol_grid_2d.rs index 4c9aa179a5..ed3f0db44c 100644 --- a/common/src/volumes/vol_grid_2d.rs +++ b/common/src/volumes/vol_grid_2d.rs @@ -89,7 +89,7 @@ impl>, V: RectRasterableVol + ReadVol + Debug> SampleVol fo impl WriteVol for VolGrid2d { #[inline(always)] - fn set(&mut self, pos: Vec3, vox: V::Vox) -> Result<(), VolGrid2dError> { + fn set(&mut self, pos: Vec3, vox: V::Vox) -> Result> { let ck = Self::chunk_key(pos); self.chunks .get_mut(&ck) diff --git a/common/src/volumes/vol_grid_3d.rs b/common/src/volumes/vol_grid_3d.rs index 30446600ff..1bb20c1d7d 100644 --- a/common/src/volumes/vol_grid_3d.rs +++ b/common/src/volumes/vol_grid_3d.rs @@ -88,7 +88,7 @@ impl>, V: RasterableVol + ReadVol + Debug> SampleVol for Vo impl WriteVol for VolGrid3d { #[inline(always)] - fn set(&mut self, pos: Vec3, vox: V::Vox) -> Result<(), VolGrid3dError> { + fn set(&mut self, pos: Vec3, vox: V::Vox) -> Result> { let ck = Self::chunk_key(pos); self.chunks .get_mut(&ck) diff --git a/common/state/src/state.rs b/common/state/src/state.rs index 1e2c8af59e..6a7f628a1e 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -436,7 +436,17 @@ impl State { } // Apply terrain changes - pub fn apply_terrain_changes(&self) { + pub fn apply_terrain_changes(&self) { self.apply_terrain_changes_internal(false); } + + /// `during_tick` is true if and only if this is called from within + /// [State::tick]. + /// + /// This only happens if [State::tick] is asked to update terrain itself + /// (using `update_terrain_and_regions: true`). [State::tick] is called + /// from within both the client and the server ticks, right after + /// handling terrain messages; currently, client sets it to true and + /// server to false. + fn apply_terrain_changes_internal(&self, during_tick: bool) { span!( _guard, "apply_terrain_changes", @@ -447,7 +457,17 @@ impl State { std::mem::take(&mut self.ecs.write_resource::().blocks); // Apply block modifications // Only include in `TerrainChanges` if successful - modified_blocks.retain(|pos, block| terrain.set(*pos, *block).is_ok()); + modified_blocks.retain(|pos, block| { + let res = terrain.set(*pos, *block); + if let (&Ok(old_block), true) = (&res, during_tick) { + // NOTE: If the changes are applied during the tick, we push the *old* value as + // the modified block (since it otherwise can't be recovered after the tick). + // Otherwise, the changes will be applied after the tick, so we push the *new* + // value. + *block = old_block; + } + res.is_ok() + }); self.ecs.write_resource::().modified_blocks = modified_blocks; } @@ -492,7 +512,7 @@ impl State { drop(guard); if update_terrain_and_regions { - self.apply_terrain_changes(); + self.apply_terrain_changes_internal(true); } // Process local events diff --git a/server/src/lib.rs b/server/src/lib.rs index eba8a14299..705f5178b5 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -553,6 +553,8 @@ impl Server { // visible to client synchronization systems, minimizing the latency of // `ServerEvent` mediated effects self.state.update_region_map(); + // NOTE: apply_terrain_changes sends the *new* value since it is not being + // synchronized during the tick. self.state.apply_terrain_changes(); let before_sync = Instant::now(); diff --git a/voxygen/src/mesh/terrain.rs b/voxygen/src/mesh/terrain.rs index fbf1b54750..8a32bc801b 100644 --- a/voxygen/src/mesh/terrain.rs +++ b/voxygen/src/mesh/terrain.rs @@ -13,7 +13,7 @@ use common::{ volumes::vol_grid_2d::{CachedVolGrid2d, VolGrid2d}, }; use common_base::span; -use std::{collections::VecDeque, fmt::Debug}; +use std::{collections::VecDeque, fmt::Debug, sync::Arc}; use tracing::error; use vek::*; @@ -233,8 +233,8 @@ impl<'a, V: RectRasterableVol + ReadVol + Debug + 'static> type Result = ( Aabb, ColLightInfo, - Box) -> f32 + Send + Sync>, - Box) -> f32 + Send + Sync>, + Arc) -> f32 + Send + Sync>, + Arc) -> f32 + Send + Sync>, ); type ShadowPipeline = ShadowPipeline; type Supplement = (Aabb, Vec2, &'a BlocksOfInterest); @@ -457,8 +457,8 @@ impl<'a, V: RectRasterableVol + ReadVol + Debug + 'static> ( bounds, (col_lights, col_lights_size), - Box::new(light), - Box::new(glow), + Arc::new(light), + Arc::new(glow), ), ) } diff --git a/voxygen/src/scene/terrain.rs b/voxygen/src/scene/terrain.rs index dd299fa892..8161c74ae8 100644 --- a/voxygen/src/scene/terrain.rs +++ b/voxygen/src/scene/terrain.rs @@ -55,6 +55,9 @@ impl Visibility { } } +/// Type of closure used for light mapping. +type LightMapFn = Arc) -> f32 + Send + Sync>; + pub struct TerrainChunkData { // GPU data load_time: f32, @@ -70,8 +73,8 @@ pub struct TerrainChunkData { /// making this an `Option`, but it probably isn't worth it since they /// shouldn't be that much more nonlocal than regular chunks). texture: Texture, - light_map: Box) -> f32 + Send + Sync>, - glow_map: Box) -> f32 + Send + Sync>, + light_map: LightMapFn, + glow_map: LightMapFn, sprite_instances: HashMap<(SpriteKind, usize), Instances>, locals: Consts, pub blocks_of_interest: BlocksOfInterest, @@ -88,19 +91,27 @@ 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), + opaque_mesh: Mesh, + fluid_mesh: Mesh, + col_lights_info: ColLightInfo, + light_map: LightMapFn, + glow_map: LightMapFn, } /// A type produced by mesh worker threads corresponding to the position and /// mesh of a chunk. struct MeshWorkerResponse { pos: Vec2, - z_bounds: (f32, f32), - opaque_mesh: Mesh, - fluid_mesh: Mesh, - col_lights_info: ColLightInfo, - light_map: Box) -> f32 + Send + Sync>, - glow_map: Box) -> f32 + Send + Sync>, sprite_instances: HashMap<(SpriteKind, usize), Vec>, + /// If None, this update was requested without meshing. + mesh: Option, started_tick: u64, blocks_of_interest: BlocksOfInterest, } @@ -147,9 +158,12 @@ impl assets::Asset for SpriteSpec { /// Function executed by worker threads dedicated to chunk meshing. #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 +/// skip_remesh is either None (do the full remesh, including recomputing the +/// light map), or Some((light_map, glow_map)). fn mesh_worker + RectRasterableVol + ReadVol + Debug + 'static>( pos: Vec2, z_bounds: (f32, f32), + skip_remesh: Option<(LightMapFn, LightMapFn)>, started_tick: u64, volume: as SampleVol>>::Sample, max_texture_size: u16, @@ -160,18 +174,31 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug + ' ) -> MeshWorkerResponse { span!(_guard, "mesh_worker"); let blocks_of_interest = BlocksOfInterest::from_chunk(&chunk); - let (opaque_mesh, fluid_mesh, _shadow_mesh, (bounds, col_lights_info, light_map, glow_map)) = - volume.generate_mesh(( - range, - Vec2::new(max_texture_size, max_texture_size), - &blocks_of_interest, - )); + 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, col_lights_info, light_map, glow_map)) = + volume.generate_mesh(( + range, + Vec2::new(max_texture_size, max_texture_size), + &blocks_of_interest, + )); + mesh = Some(MeshWorkerResponseMesh { + z_bounds: (bounds.min.z, bounds.max.z), + opaque_mesh, + fluid_mesh, + col_lights_info, + light_map, + glow_map, + }); + // Pointer juggling so borrows work out. + let mesh = mesh.as_ref().unwrap(); + (&*mesh.light_map, &*mesh.glow_map) + }; MeshWorkerResponse { pos, - z_bounds: (bounds.min.z, bounds.max.z), - opaque_mesh, - fluid_mesh, - col_lights_info, // Extract sprite locations from volume sprite_instances: { span!(_guard, "extract sprite_instances"); @@ -227,8 +254,7 @@ fn mesh_worker + RectRasterableVol + ReadVol + Debug + ' instances }, - light_map, - glow_map, + mesh, blocks_of_interest, started_tick, } @@ -617,6 +643,17 @@ impl Terrain { .unwrap_or(1.0) } + /// Determine whether a given block change actually require remeshing. + fn skip_remesh(old_block: Block, new_block: Block) -> bool { + // Both blocks are unfilled and of the same kind (this includes + // sprites within the same fluid, for example). + !new_block.is_filled() && !old_block.is_filled() && new_block.kind() == old_block.kind() && + // 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() + } + /// 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| { @@ -750,6 +787,7 @@ impl Terrain { pos, started_tick: current_tick, is_worker_active: false, + skip_remesh: false, }); } } @@ -762,7 +800,28 @@ impl Terrain { // 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, &_block) in scene_data.state.terrain_changes().modified_blocks.iter() { + 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_remesh = 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 + }; + // 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: @@ -789,6 +848,12 @@ impl Terrain { 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_remesh && !(x == 0 && y == 0) { + // We don't need to remesh neighboring chunks if this block change doesn't + // require remeshing. + continue; + } + // Only remesh if this chunk has all its neighbors let mut neighbours = true; for i in -1..2 { @@ -801,11 +866,24 @@ impl Terrain { } } if neighbours { - self.mesh_todo.insert(neighbour_chunk_pos, ChunkMeshState { - pos: neighbour_chunk_pos, - started_tick: current_tick, - is_worker_active: false, - }); + 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. The exception: if the mesh is currently + // active, we can stll set skip_remesh, since we know that means it was + // enqueued during an older tick and hence there was no intermediate update + // to this block between the enqueue and now. + todo.skip_remesh = + !todo.is_worker_active && todo.skip_remesh || skip_remesh; + todo.started_tick = current_tick; } } } @@ -882,6 +960,13 @@ impl Terrain { 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_data); @@ -896,6 +981,7 @@ impl Terrain { let _ = send.send(mesh_worker( pos, (min_z as f32, max_z as f32), + skip_remesh, started_tick, volume, max_texture_size, @@ -928,119 +1014,131 @@ impl Terrain { // 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 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 (tex, tex_size) = response.col_lights_info; - let atlas = &mut self.atlas; - let chunks = &mut self.chunks; - let col_lights = &mut self.col_lights; - let allocation = atlas - .allocate(guillotiere::Size::new( - i32::from(tex_size.x), - i32::from(tex_size.y), - )) - .unwrap_or_else(|| { - // Atlas allocation failure: try allocating a new texture and atlas. - let (new_atlas, new_col_lights) = - 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.col_lights = None; - }); - *atlas = new_atlas; - *col_lights = new_col_lights; - - atlas - .allocate(guillotiere::Size::new( - i32::from(tex_size.x), - i32::from(tex_size.y), - )) - .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 u16, - allocation.rectangle.min.y as u16, - ); - if let Err(err) = renderer.update_texture( - col_lights, - atlas_offs.into_array(), - tex_size.into_array(), - &tex, - ) { - warn!("Failed to update texture: {:?}", err); - } - - self.insert_chunk(response.pos, TerrainChunkData { - load_time, - opaque_model: renderer - .create_model(&response.opaque_mesh) - .expect("Failed to upload chunk mesh to the GPU!"), - fluid_model: if response.fluid_mesh.vertices().len() > 0 { - Some( + let sprite_instances = response + .sprite_instances + .into_iter() + .map(|(kind, instances)| { + ( + kind, renderer - .create_model(&response.fluid_mesh) - .expect("Failed to upload chunk mesh to the GPU!"), + .create_instances(&instances) + .expect("Failed to upload chunk sprite instances to the GPU!"), ) - } else { - None - }, - col_lights: Some(allocation.id), - texture: self.col_lights.clone(), - light_map: response.light_map, - glow_map: response.glow_map, - sprite_instances: response - .sprite_instances - .into_iter() - .map(|(kind, instances)| { - ( - kind, - renderer.create_instances(&instances).expect( - "Failed to upload chunk sprite instances to the GPU!", - ), + }) + .collect(); + + 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 (tex, tex_size) = mesh.col_lights_info; + let atlas = &mut self.atlas; + let chunks = &mut self.chunks; + let col_lights = &mut self.col_lights; + let allocation = atlas + .allocate(guillotiere::Size::new( + i32::from(tex_size.x), + i32::from(tex_size.y), + )) + .unwrap_or_else(|| { + // Atlas allocation failure: try allocating a new texture and atlas. + let (new_atlas, new_col_lights) = 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.col_lights = None; + }); + *atlas = new_atlas; + *col_lights = new_col_lights; + + atlas + .allocate(guillotiere::Size::new( + i32::from(tex_size.x), + i32::from(tex_size.y), + )) + .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 u16, + allocation.rectangle.min.y as u16, + ); + if let Err(err) = renderer.update_texture( + col_lights, + atlas_offs.into_array(), + tex_size.into_array(), + &tex, + ) { + warn!("Failed to update texture: {:?}", err); + } + + self.insert_chunk(response.pos, TerrainChunkData { + load_time, + opaque_model: renderer + .create_model(&mesh.opaque_mesh) + .expect("Failed to upload chunk mesh to the GPU!"), + fluid_model: if mesh.fluid_mesh.vertices().len() > 0 { + Some( + renderer + .create_model(&mesh.fluid_mesh) + .expect("Failed to upload chunk mesh to the GPU!"), ) - }) - .collect(), - locals: renderer - .create_consts(&[TerrainLocals { - model_offs: Vec3::from( - response.pos.map2(VolGrid2d::::chunk_size(), |e, sz| { - e as f32 * sz as f32 - }), - ) - .into_array(), - atlas_offs: Vec4::new( - i32::from(atlas_offs.x), - i32::from(atlas_offs.y), - 0, - 0, - ) - .into_array(), - load_time, - }]) - .expect("Failed to upload chunk locals to the GPU!"), - 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: response.z_bounds, - frustum_last_plane_index: 0, - }); + } else { + None + }, + col_lights: Some(allocation.id), + texture: self.col_lights.clone(), + light_map: mesh.light_map, + glow_map: mesh.glow_map, + sprite_instances, + locals: renderer + .create_consts(&[TerrainLocals { + model_offs: Vec3::from( + response.pos.map2(VolGrid2d::::chunk_size(), |e, sz| { + e as f32 * sz as f32 + }), + ) + .into_array(), + atlas_offs: Vec4::new( + i32::from(atlas_offs.x), + i32::from(atlas_offs.y), + 0, + 0, + ) + .into_array(), + load_time, + }]) + .expect("Failed to upload chunk locals to the GPU!"), + 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, + frustum_last_plane_index: 0, + }); + } 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);