Merge branch 'sharp/skip-sprite-mesh' into 'master'

Don't remesh chunk vertex data on sprite update.

See merge request veloren/veloren!2294
This commit is contained in:
Joshua Yanovski 2021-05-11 20:39:04 +00:00
commit 45367b192f
11 changed files with 281 additions and 159 deletions

View File

@ -53,9 +53,9 @@ impl<T> Grid<T> {
self.cells.get_mut(idx)
}
pub fn set(&mut self, pos: Vec2<i32>, cell: T) -> Option<()> {
pub fn set(&mut self, pos: Vec2<i32>, cell: T) -> Option<T> {
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<Item = (Vec2<i32>, &T)> + '_ {

View File

@ -167,13 +167,13 @@ impl<V, S: RectVolSize, M: Clone> ReadVol for Chonk<V, S, M> {
impl<V: Clone + PartialEq, S: RectVolSize, M: Clone> WriteVol for Chonk<V, S, M> {
#[inline(always)]
fn set(&mut self, pos: Vec3<i32>, block: Self::Vox) -> Result<(), Self::Error> {
fn set(&mut self, pos: Vec3<i32>, block: Self::Vox) -> Result<V, Self::Error> {
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::<V, SubChunkSize<S>, M>::filled(self.below.clone(), self.meta.clone());
@ -184,7 +184,7 @@ impl<V: Clone + PartialEq, S: RectVolSize, M: Clone> WriteVol for Chonk<V, S, M>
} 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::<V, SubChunkSize<S>, M>::filled(self.above.clone(), self.meta.clone());

View File

@ -133,7 +133,7 @@ pub trait SampleVol<I>: 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<i32>, vox: Self::Vox) -> Result<(), Self::Error>;
fn set(&mut self, pos: Vec3<i32>, vox: Self::Vox) -> Result<Self::Vox, Self::Error>;
/// 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<i32>,
f: F,
) -> Result<(), Self::Error>
) -> Result<Self::Vox, Self::Error>
where
Self: ReadVol,
Self::Vox: Clone,

View File

@ -271,15 +271,17 @@ impl<V, S: VolSize, M> Chunk<V, S, M> {
}
#[inline(always)]
fn set_unchecked(&mut self, pos: Vec3<i32>, vox: V)
fn set_unchecked(&mut self, pos: Vec3<i32>, 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<V, S: VolSize, M> ReadVol for Chunk<V, S, M> {
impl<V: Clone + PartialEq, S: VolSize, M> WriteVol for Chunk<V, S, M> {
#[inline(always)]
#[allow(clippy::unit_arg)] // TODO: Pending review in #587
fn set(&mut self, pos: Vec3<i32>, vox: Self::Vox) -> Result<(), Self::Error> {
fn set(&mut self, pos: Vec3<i32>, vox: Self::Vox) -> Result<Self::Vox, Self::Error> {
if !pos
.map2(S::SIZE, |e, s| 0 <= e && e < s as i32)
.reduce_and()

View File

@ -93,10 +93,10 @@ impl<V, M, A: Access> ReadVol for Dyna<V, M, A> {
impl<V, M, A: Access> WriteVol for Dyna<V, M, A> {
#[inline(always)]
fn set(&mut self, pos: Vec3<i32>, vox: Self::Vox) -> Result<(), DynaError> {
fn set(&mut self, pos: Vec3<i32>, vox: Self::Vox) -> Result<Self::Vox, DynaError> {
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)
}
}

View File

@ -89,7 +89,7 @@ impl<I: Into<Aabr<i32>>, V: RectRasterableVol + ReadVol + Debug> SampleVol<I> fo
impl<V: RectRasterableVol + WriteVol + Clone + Debug> WriteVol for VolGrid2d<V> {
#[inline(always)]
fn set(&mut self, pos: Vec3<i32>, vox: V::Vox) -> Result<(), VolGrid2dError<V>> {
fn set(&mut self, pos: Vec3<i32>, vox: V::Vox) -> Result<V::Vox, VolGrid2dError<V>> {
let ck = Self::chunk_key(pos);
self.chunks
.get_mut(&ck)

View File

@ -88,7 +88,7 @@ impl<I: Into<Aabb<i32>>, V: RasterableVol + ReadVol + Debug> SampleVol<I> for Vo
impl<V: RasterableVol + WriteVol + Clone + Debug> WriteVol for VolGrid3d<V> {
#[inline(always)]
fn set(&mut self, pos: Vec3<i32>, vox: V::Vox) -> Result<(), VolGrid3dError<V>> {
fn set(&mut self, pos: Vec3<i32>, vox: V::Vox) -> Result<V::Vox, VolGrid3dError<V>> {
let ck = Self::chunk_key(pos);
self.chunks
.get_mut(&ck)

View File

@ -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::<BlockChange>().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::<TerrainChanges>().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

View File

@ -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();

View File

@ -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<Vox = Block> + ReadVol + Debug + 'static>
type Result = (
Aabb<f32>,
ColLightInfo,
Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
Arc<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
);
type ShadowPipeline = ShadowPipeline;
type Supplement = (Aabb<i32>, Vec2<u16>, &'a BlocksOfInterest);
@ -457,8 +457,8 @@ impl<'a, V: RectRasterableVol<Vox = Block> + ReadVol + Debug + 'static>
(
bounds,
(col_lights, col_lights_size),
Box::new(light),
Box::new(glow),
Arc::new(light),
Arc::new(glow),
),
)
}

View File

@ -55,6 +55,9 @@ impl Visibility {
}
}
/// Type of closure used for light mapping.
type LightMapFn = Arc<dyn Fn(Vec3<i32>) -> 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<ColLightFmt>,
light_map: Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
glow_map: Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
light_map: LightMapFn,
glow_map: LightMapFn,
sprite_instances: HashMap<(SpriteKind, usize), Instances<SpriteInstance>>,
locals: Consts<TerrainLocals>,
pub blocks_of_interest: BlocksOfInterest,
@ -88,19 +91,27 @@ struct ChunkMeshState {
pos: Vec2<i32>,
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<TerrainPipeline>,
fluid_mesh: Mesh<FluidPipeline>,
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<i32>,
z_bounds: (f32, f32),
opaque_mesh: Mesh<TerrainPipeline>,
fluid_mesh: Mesh<FluidPipeline>,
col_lights_info: ColLightInfo,
light_map: Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
glow_map: Box<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>,
sprite_instances: HashMap<(SpriteKind, usize), Vec<SpriteInstance>>,
/// If None, this update was requested without meshing.
mesh: Option<MeshWorkerResponseMesh>,
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<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + 'static>(
pos: Vec2<i32>,
z_bounds: (f32, f32),
skip_remesh: Option<(LightMapFn, LightMapFn)>,
started_tick: u64,
volume: <VolGrid2d<V> as SampleVol<Aabr<i32>>>::Sample,
max_texture_size: u16,
@ -160,18 +174,31 @@ fn mesh_worker<V: BaseVol<Vox = Block> + 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<V: BaseVol<Vox = Block> + RectRasterableVol + ReadVol + Debug + '
instances
},
light_map,
glow_map,
mesh,
blocks_of_interest,
started_tick,
}
@ -617,6 +643,17 @@ impl<V: RectRasterableVol> Terrain<V> {
.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<i32>) -> f32 {
let chunk_pos = Vec2::from(wpos).map2(TerrainChunk::RECT_SIZE, |e: i32, sz| {
@ -750,6 +787,7 @@ impl<V: RectRasterableVol> Terrain<V> {
pos,
started_tick: current_tick,
is_worker_active: false,
skip_remesh: false,
});
}
}
@ -762,7 +800,28 @@ impl<V: RectRasterableVol> Terrain<V> {
// 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<V: RectRasterableVol> Terrain<V> {
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<V: RectRasterableVol> Terrain<V> {
}
}
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<V: RectRasterableVol> Terrain<V> {
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<V: RectRasterableVol> Terrain<V> {
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<V: RectRasterableVol> Terrain<V> {
// 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::<V>::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::<V>::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);