mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
d87225d908
Firstly, most importantly, improves the heuristic used for deciding which chunks to mesh (which matters more even at low view distances with meshing being so expensive now, but has an even more obvious improvement at large view distances). Essentially, instead of always prioritizing whatever chunk was fetched earliest from the server, instead we prioritize chunks *closest* to the player first, then chunk order. This greatly improves the apparent latency for things like picking up a sprite, as well as cases where the player moves out of the loaded range but (due to slow loading from the server or a large VD range) there are many remaining chunks left to be meshed still within the VD but nowhere near the player. By properly priotizing chunks near the player, we minimize the time / likelihood of a player being on or very near an unmeshed chunk, and make high VDs and faster travel speeds more viable. We make a few other minor improvements as well: Avoid duplicate meshing of neighbors when first inserting chunks, if they are already in the todo list and the chunk being inserted was not directly modified. Also avoid remeshing neighbors if only a solid block's color changed, which could sometimes be useful for non-sprite modifications (for example flame-induced changes to non-destructible terrain color).
1574 lines
67 KiB
Rust
1574 lines
67 KiB
Rust
mod watcher;
|
|
pub use self::watcher::{BlocksOfInterest, Interaction};
|
|
|
|
use crate::{
|
|
mesh::{greedy::GreedyMesh, terrain::SUNLIGHT, Meshable},
|
|
render::{
|
|
ColLightFmt, ColLightInfo, Consts, FluidPipeline, GlobalModel, Instances, Mesh, Model,
|
|
RenderError, Renderer, ShadowPipeline, SpriteInstance, SpriteLocals, SpritePipeline,
|
|
TerrainLocals, TerrainPipeline, Texture,
|
|
},
|
|
};
|
|
|
|
use super::{math, LodData, SceneData};
|
|
use common::{
|
|
assets::{self, AssetExt, DotVoxAsset},
|
|
figure::Segment,
|
|
spiral::Spiral2d,
|
|
terrain::{sprite, Block, SpriteKind, TerrainChunk},
|
|
vol::{BaseVol, ReadVol, RectRasterableVol, SampleVol},
|
|
volumes::vol_grid_2d::{VolGrid2d, VolGrid2dError},
|
|
};
|
|
use common_base::span;
|
|
use core::{f32, fmt::Debug, i32, marker::PhantomData, time::Duration};
|
|
use crossbeam::channel;
|
|
use enum_iterator::IntoEnumIterator;
|
|
use guillotiere::AtlasAllocator;
|
|
use hashbrown::HashMap;
|
|
use serde::Deserialize;
|
|
use std::sync::{
|
|
atomic::{AtomicU64, Ordering},
|
|
Arc,
|
|
};
|
|
use tracing::warn;
|
|
use treeculler::{BVol, Frustum, AABB};
|
|
use vek::*;
|
|
|
|
const SPRITE_SCALE: Vec3<f32> = Vec3::new(1.0 / 11.0, 1.0 / 11.0, 1.0 / 11.0);
|
|
|
|
#[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<dyn Fn(Vec3<i32>) -> f32 + Send + Sync>;
|
|
|
|
pub struct TerrainChunkData {
|
|
// GPU data
|
|
load_time: f32,
|
|
opaque_model: Model<TerrainPipeline>,
|
|
fluid_model: Option<Model<FluidPipeline>>,
|
|
/// If this is `None`, this texture is not allocated in the current atlas,
|
|
/// and therefore there is no need to free its allocation.
|
|
col_lights: Option<guillotiere::AllocId>,
|
|
/// 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).
|
|
texture: Texture<ColLightFmt>,
|
|
light_map: LightMapFn,
|
|
glow_map: LightMapFn,
|
|
sprite_instances: HashMap<(SpriteKind, usize), Instances<SpriteInstance>>,
|
|
locals: Consts<TerrainLocals>,
|
|
pub blocks_of_interest: BlocksOfInterest,
|
|
|
|
visible: Visibility,
|
|
can_shadow_point: bool,
|
|
can_shadow_sun: bool,
|
|
z_bounds: (f32, f32),
|
|
frustum_last_plane_index: u8,
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
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>,
|
|
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,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
/// Configuration data for an individual sprite model.
|
|
struct SpriteModelConfig<Model> {
|
|
/// Data for the .vox model associated with this sprite.
|
|
model: Model,
|
|
/// Sprite model center (as an offset from 0 in the .vox file).
|
|
offset: (f32, f32, f32),
|
|
/// LOD axes (how LOD gets applied along each axis, when we switch
|
|
/// to an LOD model).
|
|
lod_axes: (f32, f32, f32),
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
/// Configuration data for a group of sprites (currently associated with a
|
|
/// particular SpriteKind).
|
|
struct SpriteConfig<Model> {
|
|
/// All possible model variations for this sprite.
|
|
// NOTE: Could make constant per sprite type, but eliminating this indirection and
|
|
// allocation is probably not that important considering how sprites are used.
|
|
variations: Vec<SpriteModelConfig<Model>>,
|
|
/// The extent to which the sprite sways in the window.
|
|
///
|
|
/// 0.0 is normal.
|
|
wind_sway: f32,
|
|
}
|
|
|
|
/// Configuration data for all sprite models.
|
|
///
|
|
/// NOTE: Model is an asset path to the appropriate sprite .vox model.
|
|
#[derive(Deserialize)]
|
|
#[serde(transparent)]
|
|
struct SpriteSpec(sprite::sprite_kind::PureCases<Option<SpriteConfig<String>>>);
|
|
|
|
impl assets::Asset for SpriteSpec {
|
|
type Loader = assets::RonLoader;
|
|
|
|
const EXTENSION: &'static str = "ron";
|
|
}
|
|
|
|
/// 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,
|
|
chunk: Arc<TerrainChunk>,
|
|
range: Aabb<i32>,
|
|
sprite_data: &HashMap<(SpriteKind, usize), Vec<SpriteData>>,
|
|
sprite_config: &SpriteSpec,
|
|
) -> MeshWorkerResponse {
|
|
span!(_guard, "mesh_worker");
|
|
let blocks_of_interest = BlocksOfInterest::from_chunk(&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, 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 {
|
|
// TODO: Take sprite bounds into account somehow?
|
|
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,
|
|
// Extract sprite locations from volume
|
|
sprite_instances: {
|
|
span!(_guard, "extract sprite_instances");
|
|
let mut instances = HashMap::new();
|
|
|
|
for x in 0..V::RECT_SIZE.x as i32 {
|
|
for y in 0..V::RECT_SIZE.y as i32 {
|
|
for z in z_bounds.0 as i32..z_bounds.1 as i32 + 1 {
|
|
let rel_pos = Vec3::new(x, y, z);
|
|
let wpos = Vec3::from(pos * V::RECT_SIZE.map(|e: u32| e as i32)) + rel_pos;
|
|
|
|
let block = if let Ok(block) = volume.get(wpos) {
|
|
block
|
|
} else {
|
|
continue;
|
|
};
|
|
let sprite = if let Some(sprite) = block.get_sprite() {
|
|
sprite
|
|
} else {
|
|
continue;
|
|
};
|
|
|
|
if let Some(cfg) = sprite.elim_case_pure(&sprite_config.0) {
|
|
let seed = wpos.x as u64 * 3
|
|
+ wpos.y as u64 * 7
|
|
+ wpos.x as u64 * wpos.y as u64; // Awful PRNG
|
|
let ori = (block.get_ori().unwrap_or((seed % 4) as u8 * 2)) & 0b111;
|
|
let variation = seed as usize % cfg.variations.len();
|
|
let key = (sprite, variation);
|
|
// NOTE: Safe because we called sprite_config_for already.
|
|
// NOTE: Safe because 0 ≤ ori < 8
|
|
let sprite_data = &sprite_data[&key][0];
|
|
let instance = SpriteInstance::new(
|
|
Mat4::identity()
|
|
.translated_3d(sprite_data.offset)
|
|
.rotated_z(f32::consts::PI * 0.25 * ori as f32)
|
|
.translated_3d(
|
|
(rel_pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0))
|
|
/ SPRITE_SCALE,
|
|
),
|
|
cfg.wind_sway,
|
|
rel_pos,
|
|
ori,
|
|
light_map(wpos),
|
|
glow_map(wpos),
|
|
);
|
|
|
|
instances.entry(key).or_insert(Vec::new()).push(instance);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
instances
|
|
},
|
|
mesh,
|
|
blocks_of_interest,
|
|
started_tick,
|
|
}
|
|
}
|
|
|
|
struct SpriteData {
|
|
/* mat: Mat4<f32>, */
|
|
locals: Consts<SpriteLocals>,
|
|
model: Model<SpritePipeline>,
|
|
/* scale: Vec3<f32>, */
|
|
offset: Vec3<f32>,
|
|
}
|
|
|
|
pub struct Terrain<V: RectRasterableVol = TerrainChunk> {
|
|
/// 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,
|
|
/// FIXME: This could possibly become an `AssetHandle<SpriteSpec>`, to get
|
|
/// hot-reloading for free, but I am not sure if sudden changes of this
|
|
/// value would break something
|
|
sprite_config: Arc<SpriteSpec>,
|
|
chunks: HashMap<Vec2<i32>, 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<i32>, TerrainChunkData)>,
|
|
/* /// Secondary index into the terrain chunk table, used to sort through chunks by z index from
|
|
/// the top down.
|
|
z_index_down: BTreeSet<Vec3<i32>>,
|
|
/// Secondary index into the terrain chunk table, used to sort through chunks by z index from
|
|
/// the bottom up.
|
|
z_index_up: BTreeSet<Vec3<i32>>, */
|
|
// 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<MeshWorkerResponse>,
|
|
mesh_recv: channel::Receiver<MeshWorkerResponse>,
|
|
mesh_todo: HashMap<Vec2<i32>, ChunkMeshState>,
|
|
mesh_todos_active: Arc<AtomicU64>,
|
|
mesh_recv_overflow: f32,
|
|
|
|
// GPU data
|
|
sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
|
|
sprite_col_lights: Texture<ColLightFmt>,
|
|
/// 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.
|
|
col_lights: Texture<ColLightFmt>,
|
|
waves: Texture,
|
|
|
|
phantom: PhantomData<V>,
|
|
}
|
|
|
|
impl TerrainChunkData {
|
|
pub fn can_shadow_sun(&self) -> bool { self.visible.is_visible() || self.can_shadow_sun }
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub struct SpriteRenderContext {
|
|
sprite_config: Arc<SpriteSpec>,
|
|
sprite_data: Arc<HashMap<(SpriteKind, usize), Vec<SpriteData>>>,
|
|
sprite_col_lights: Texture<ColLightFmt>,
|
|
}
|
|
|
|
pub type SpriteRenderContextLazy = Box<dyn FnMut(&mut Renderer) -> SpriteRenderContext>;
|
|
|
|
impl SpriteRenderContext {
|
|
#[allow(clippy::float_cmp)] // TODO: Pending review in #587
|
|
pub fn new(renderer: &mut Renderer) -> SpriteRenderContextLazy {
|
|
let max_texture_size = renderer.max_texture_size();
|
|
|
|
struct SpriteDataResponse {
|
|
locals: [SpriteLocals; 8],
|
|
model: Mesh<SpritePipeline>,
|
|
offset: Vec3<f32>,
|
|
}
|
|
|
|
struct SpriteWorkerResponse {
|
|
sprite_config: Arc<SpriteSpec>,
|
|
sprite_data: HashMap<(SpriteKind, usize), Vec<SpriteDataResponse>>,
|
|
sprite_col_lights: ColLightInfo,
|
|
}
|
|
|
|
let join_handle = std::thread::spawn(move || {
|
|
// Load all the sprite config data.
|
|
let sprite_config =
|
|
Arc::<SpriteSpec>::load_expect("voxygen.voxel.sprite_manifest").cloned();
|
|
|
|
let max_size =
|
|
guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size));
|
|
let mut greedy = GreedyMesh::new(max_size);
|
|
let mut locals_buffer = [SpriteLocals::default(); 8];
|
|
let sprite_config_ = &sprite_config;
|
|
// NOTE: Tracks the start vertex of the next model to be meshed.
|
|
|
|
let sprite_data: HashMap<(SpriteKind, usize), _> = SpriteKind::into_enum_iter()
|
|
.filter_map(|kind| Some((kind, kind.elim_case_pure(&sprite_config_.0).as_ref()?)))
|
|
.flat_map(|(kind, sprite_config)| {
|
|
let wind_sway = sprite_config.wind_sway;
|
|
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
|
|
}
|
|
});
|
|
let sprite_mat: Mat4<f32> =
|
|
Mat4::translation_3d(offset).scaled_3d(SPRITE_SCALE);
|
|
move |greedy: &mut GreedyMesh| {
|
|
(
|
|
(kind, variation),
|
|
scaled
|
|
.iter()
|
|
.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 })
|
|
};
|
|
// Mesh generation exclusively acts using side effects; it
|
|
// has no
|
|
// interesting return value, but updates the mesh.
|
|
let mut opaque_mesh = Mesh::new();
|
|
Meshable::<SpritePipeline, &mut GreedyMesh>::generate_mesh(
|
|
Segment::from(&model.read().0).scaled_by(lod_scale),
|
|
(greedy, &mut opaque_mesh, false),
|
|
);
|
|
|
|
let sprite_scale = Vec3::one() / lod_scale;
|
|
let sprite_mat: Mat4<f32> =
|
|
sprite_mat * Mat4::scaling_3d(sprite_scale);
|
|
locals_buffer.iter_mut().enumerate().for_each(
|
|
|(ori, locals)| {
|
|
let sprite_mat = sprite_mat
|
|
.rotated_z(f32::consts::PI * 0.25 * ori as f32);
|
|
*locals = SpriteLocals::new(
|
|
sprite_mat,
|
|
sprite_scale,
|
|
offset,
|
|
wind_sway,
|
|
);
|
|
},
|
|
);
|
|
|
|
SpriteDataResponse {
|
|
model: opaque_mesh,
|
|
offset,
|
|
locals: locals_buffer,
|
|
}
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
)
|
|
}
|
|
},
|
|
)
|
|
})
|
|
.map(|mut f| f(&mut greedy))
|
|
.collect();
|
|
|
|
let sprite_col_lights = greedy.finalize();
|
|
|
|
SpriteWorkerResponse {
|
|
sprite_config,
|
|
sprite_data,
|
|
sprite_col_lights,
|
|
}
|
|
});
|
|
|
|
let init = core::lazy::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_col_lights,
|
|
} = 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_data = sprite_data
|
|
.into_iter()
|
|
.map(|(key, models)| {
|
|
(
|
|
key,
|
|
models
|
|
.into_iter()
|
|
.map(
|
|
|SpriteDataResponse {
|
|
locals,
|
|
model,
|
|
offset,
|
|
}| {
|
|
SpriteData {
|
|
locals: renderer
|
|
.create_consts(&locals)
|
|
.expect("Failed to upload sprite locals to the GPU!"),
|
|
model: renderer.create_model(&model).expect(
|
|
"Failed to upload sprite model data to the GPU!",
|
|
),
|
|
offset,
|
|
}
|
|
},
|
|
)
|
|
.collect(),
|
|
)
|
|
})
|
|
.collect();
|
|
let sprite_col_lights = ShadowPipeline::create_col_lights(renderer, &sprite_col_lights)
|
|
.expect("Failed to upload sprite color and light data to the GPU!");
|
|
|
|
Self {
|
|
sprite_config: Arc::clone(&sprite_config),
|
|
sprite_data: Arc::new(sprite_data),
|
|
sprite_col_lights,
|
|
}
|
|
};
|
|
Box::new(move |renderer| init.get_or_init(|| closure(renderer)).clone())
|
|
}
|
|
}
|
|
|
|
impl<V: RectRasterableVol> Terrain<V> {
|
|
pub fn new(renderer: &mut Renderer, 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, col_lights) =
|
|
Self::make_atlas(renderer).expect("Failed to create atlas texture");
|
|
|
|
Self {
|
|
atlas,
|
|
sprite_config: sprite_render_context.sprite_config,
|
|
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_data: sprite_render_context.sprite_data,
|
|
sprite_col_lights: sprite_render_context.sprite_col_lights,
|
|
waves: renderer
|
|
.create_texture(
|
|
&assets::Image::load_expect("voxygen.texture.waves").read().0,
|
|
Some(gfx::texture::FilterMethod::Trilinear),
|
|
Some(gfx::texture::WrapMode::Tile),
|
|
None,
|
|
)
|
|
.expect("Failed to create wave texture"),
|
|
col_lights,
|
|
phantom: PhantomData,
|
|
}
|
|
}
|
|
|
|
fn make_atlas(
|
|
renderer: &mut Renderer,
|
|
) -> Result<(AtlasAllocator, Texture<ColLightFmt>), RenderError> {
|
|
span!(_guard, "make_atlas", "Terrain::make_atlas");
|
|
let max_texture_size = renderer.max_texture_size();
|
|
let atlas_size =
|
|
guillotiere::Size::new(i32::from(max_texture_size), i32::from(max_texture_size));
|
|
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 texture = renderer.create_texture_raw(
|
|
gfx::texture::Kind::D2(
|
|
max_texture_size,
|
|
max_texture_size,
|
|
gfx::texture::AaMode::Single,
|
|
),
|
|
1_u8,
|
|
gfx::memory::Bind::SHADER_RESOURCE,
|
|
gfx::memory::Usage::Dynamic,
|
|
(0, 0),
|
|
gfx::format::Swizzle::new(),
|
|
gfx::texture::SamplerInfo::new(
|
|
gfx::texture::FilterMethod::Bilinear,
|
|
gfx::texture::WrapMode::Clamp,
|
|
),
|
|
)?;
|
|
Ok((atlas, texture))
|
|
}
|
|
|
|
fn remove_chunk_meta(&mut self, _pos: Vec2<i32>, 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(col_lights) = chunk.col_lights {
|
|
self.atlas.deallocate(col_lights);
|
|
}
|
|
/* 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<i32>, 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<i32>) {
|
|
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<i32>) -> 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<i32>) -> 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<f32>) -> (Vec3<f32>, 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)
|
|
.map(|rpos| {
|
|
let chunk_pos = wpos_chunk + rpos;
|
|
self.chunks
|
|
.get(&chunk_pos)
|
|
.map(|c| c.blocks_of_interest.lights.iter())
|
|
.into_iter()
|
|
.flatten()
|
|
.map(move |(lpos, level)| {
|
|
(
|
|
Vec3::<i32>::from(
|
|
chunk_pos * TerrainChunk::RECT_SIZE.map(|e| e as i32),
|
|
) + *lpos,
|
|
level,
|
|
)
|
|
})
|
|
})
|
|
.flatten()
|
|
.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 as f32;
|
|
(
|
|
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.
|
|
#[allow(clippy::for_loops_over_fallibles)] // TODO: Pending review in #587
|
|
#[allow(clippy::len_zero)] // TODO: Pending review in #587
|
|
pub fn maintain(
|
|
&mut self,
|
|
renderer: &mut Renderer,
|
|
scene_data: &SceneData,
|
|
focus_pos: Vec3<f32>,
|
|
loaded_distance: f32,
|
|
view_mat: Mat4<f32>,
|
|
proj_mat: Mat4<f32>,
|
|
) -> (Aabb<f32>, Vec<math::Vec3<f32>>, math::Aabr<f32>) {
|
|
// 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();
|
|
let mut visible_bounding_box: Option<Aabb<f32>> = 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()
|
|
.get_key(pos + Vec2::new(i, j))
|
|
.is_some();
|
|
}
|
|
}
|
|
|
|
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()
|
|
.get_key(neighbour_chunk_pos + Vec2::new(i, j))
|
|
.is_some();
|
|
}
|
|
}
|
|
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_::<i64>();
|
|
for (todo, chunk) in self
|
|
.mesh_todo
|
|
.values_mut()
|
|
.filter(|todo| !todo.is_worker_active)
|
|
.min_by_key(|todo| ((todo.pos.as_::<i64>() * TerrainChunk::RECT_SIZE.as_::<i64>()).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;
|
|
}
|
|
|
|
// Find the area of the terrain we want. Because meshing needs to compute things
|
|
// 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::<V>::chunk_size(), |e, sz| e * sz as i32 - 1),
|
|
max: todo.pos.map2(VolGrid2d::<V>::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_data);
|
|
let sprite_config = Arc::clone(&self.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,
|
|
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::<Vec<_>>(); // 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
|
|
.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!"),
|
|
)
|
|
} 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);
|
|
}
|
|
},
|
|
// 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 * 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_::<f32>() * 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::<f32>::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.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_box = Aabb {
|
|
min: Vec3::from(chunk_min),
|
|
max: Vec3::from(chunk_max),
|
|
};
|
|
|
|
if in_frustum {
|
|
let visible_box = chunk_box;
|
|
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,
|
|
});
|
|
|
|
// PSCs: Potential shadow casters
|
|
let ray_direction = scene_data.get_sun_dir();
|
|
let collides_with_aabr = |a: math::Aabb<f32>, b: math::Aabr<f32>| {
|
|
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.render_mode().shadow.is_map()
|
|
{
|
|
let visible_bounding_box = math::Aabb::<f32> {
|
|
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_::<f64>();
|
|
let inv_proj_view =
|
|
math::Mat4::from_col_arrays((proj_mat * view_mat).into_col_arrays())
|
|
.as_::<f64>()
|
|
.inverted();
|
|
let ray_direction = math::Vec3::<f32>::from(ray_direction);
|
|
let visible_light_volume = math::calc_focused_light_volume_points(
|
|
inv_proj_view,
|
|
ray_direction.as_::<f64>(),
|
|
visible_bounds_fine,
|
|
1e-6,
|
|
)
|
|
.map(|v| v.as_::<f32>())
|
|
.collect::<Vec<_>>();
|
|
|
|
let cam_pos = math::Vec4::from(view_mat.inverted() * Vec4::unit_w()).xyz();
|
|
let up: math::Vec3<f32> = { math::Vec3::unit_y() };
|
|
|
|
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<i32>, chunk: &TerrainChunkData| {
|
|
let chunk_pos = pos.as_::<f32>() * 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);
|
|
|
|
(
|
|
visible_bounding_box,
|
|
visible_light_volume,
|
|
visible_psr_bounds,
|
|
)
|
|
}
|
|
|
|
pub fn get(&self, chunk_key: Vec2<i32>) -> 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(
|
|
&self,
|
|
renderer: &mut Renderer,
|
|
global: &GlobalModel,
|
|
(is_daylight, light_data): super::LightData,
|
|
focus_pos: Vec3<f32>,
|
|
) {
|
|
span!(_guard, "render_shadows", "Terrain::render_shadows");
|
|
if !renderer.render_mode().shadow.is_map() {
|
|
return;
|
|
};
|
|
|
|
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).
|
|
if is_daylight {
|
|
chunk_iter
|
|
.clone()
|
|
.filter(|chunk| chunk.can_shadow_sun())
|
|
.chain(self.shadow_chunks.iter().map(|(_, chunk)| chunk))
|
|
.for_each(|chunk| {
|
|
// Directed light shadows.
|
|
renderer.render_terrain_shadow_directed(
|
|
&chunk.opaque_model,
|
|
global,
|
|
&chunk.locals,
|
|
&global.shadow_mats,
|
|
);
|
|
});
|
|
}
|
|
|
|
// Point shadows
|
|
//
|
|
// NOTE: We don't bother retaining chunks unless they cast sun shadows, so we
|
|
// don't use `shadow_chunks` here.
|
|
light_data.iter().take(1).for_each(|_light| {
|
|
chunk_iter.clone().for_each(|chunk| {
|
|
if chunk.can_shadow_point {
|
|
renderer.render_shadow_point(
|
|
&chunk.opaque_model,
|
|
global,
|
|
&chunk.locals,
|
|
&global.shadow_mats,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
pub fn render(
|
|
&self,
|
|
renderer: &mut Renderer,
|
|
global: &GlobalModel,
|
|
lod: &LodData,
|
|
focus_pos: Vec3<f32>,
|
|
) {
|
|
span!(_guard, "render", "Terrain::render");
|
|
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).map(|c| (pos, c))
|
|
})
|
|
.take(self.chunks.len());
|
|
|
|
for (_, chunk) in chunk_iter {
|
|
if chunk.visible.is_visible() {
|
|
renderer.render_terrain_chunk(
|
|
&chunk.opaque_model,
|
|
&chunk.texture,
|
|
global,
|
|
&chunk.locals,
|
|
lod,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn render_translucent(
|
|
&self,
|
|
renderer: &mut Renderer,
|
|
global: &GlobalModel,
|
|
lod: &LodData,
|
|
focus_pos: Vec3<f32>,
|
|
cam_pos: Vec3<f32>,
|
|
sprite_render_distance: f32,
|
|
) {
|
|
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());
|
|
|
|
// Terrain sprites
|
|
// TODO: move to separate functions
|
|
span!(guard, "Terrain sprites");
|
|
let chunk_size = V::RECT_SIZE.map(|e| e as f32);
|
|
let chunk_mag = (chunk_size * (f32::consts::SQRT_2 * 0.5)).magnitude_squared();
|
|
for (pos, chunk) in chunk_iter.clone() {
|
|
if chunk.visible.is_visible() {
|
|
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;
|
|
|
|
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 =
|
|
Vec2::from(cam_pos)
|
|
.distance_squared(chunk_center)
|
|
.min(Vec2::from(cam_pos).distance_squared(chunk_center - chunk_size * 0.5))
|
|
.min(Vec2::from(cam_pos).distance_squared(
|
|
chunk_center - chunk_size.x * 0.5 + chunk_size.y * 0.5,
|
|
))
|
|
.min(
|
|
Vec2::from(cam_pos).distance_squared(chunk_center + chunk_size.x * 0.5),
|
|
)
|
|
.min(Vec2::from(cam_pos).distance_squared(
|
|
chunk_center + chunk_size.x * 0.5 - chunk_size.y * 0.5,
|
|
));
|
|
if focus_dist_sqrd < sprite_render_distance.powi(2) {
|
|
for (kind, instances) in (&chunk.sprite_instances).into_iter() {
|
|
let SpriteData { model, locals, .. } = if kind
|
|
.0
|
|
.elim_case_pure(&self.sprite_config.0)
|
|
.as_ref()
|
|
.map(|config| config.wind_sway >= 0.4)
|
|
.unwrap_or(false)
|
|
&& dist_sqrd <= chunk_mag
|
|
|| dist_sqrd < sprite_high_detail_distance.powi(2)
|
|
{
|
|
&self.sprite_data[&kind][0]
|
|
} else if dist_sqrd < sprite_hid_detail_distance.powi(2) {
|
|
&self.sprite_data[&kind][1]
|
|
} else if dist_sqrd < sprite_mid_detail_distance.powi(2) {
|
|
&self.sprite_data[&kind][2]
|
|
} else if dist_sqrd < sprite_low_detail_distance.powi(2) {
|
|
&self.sprite_data[&kind][3]
|
|
} else {
|
|
&self.sprite_data[&kind][4]
|
|
};
|
|
renderer.render_sprites(
|
|
model,
|
|
&self.sprite_col_lights,
|
|
global,
|
|
&chunk.locals,
|
|
locals,
|
|
&instances,
|
|
lod,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
drop(guard);
|
|
|
|
// Translucent
|
|
chunk_iter
|
|
.clone()
|
|
.filter(|(_, chunk)| chunk.visible.is_visible())
|
|
.filter_map(|(_, chunk)| {
|
|
chunk
|
|
.fluid_model
|
|
.as_ref()
|
|
.map(|model| (model, &chunk.locals))
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.into_iter()
|
|
.rev() // Render back-to-front
|
|
.for_each(|(model, locals)| {
|
|
renderer.render_fluid_chunk(
|
|
model,
|
|
global,
|
|
locals,
|
|
lod,
|
|
&self.waves,
|
|
)
|
|
});
|
|
}
|
|
}
|