2019-01-15 15:13:11 +00:00
|
|
|
use crate::{
|
2019-01-23 20:01:58 +00:00
|
|
|
mesh::Meshable,
|
2019-04-29 20:37:19 +00:00
|
|
|
render::{Consts, Globals, Mesh, Model, Renderer, TerrainLocals, TerrainPipeline},
|
2019-01-15 15:13:11 +00:00
|
|
|
};
|
2019-05-17 17:54:56 +00:00
|
|
|
use client::Client;
|
2019-06-06 07:13:58 +00:00
|
|
|
use common::{
|
|
|
|
terrain::{TerrainChunkSize, TerrainMap},
|
|
|
|
vol::{SampleVol, VolSize},
|
|
|
|
volumes::vol_map_2d::VolMap2dErr,
|
|
|
|
};
|
2019-06-06 09:04:37 +00:00
|
|
|
use frustum_query::frustum::Frustum;
|
2019-07-04 16:33:59 +00:00
|
|
|
use fxhash::FxHashMap;
|
2019-07-04 17:13:29 +00:00
|
|
|
use std::{i32, ops::Mul, sync::mpsc, time::Duration};
|
2019-05-17 17:54:56 +00:00
|
|
|
use vek::*;
|
2019-01-15 15:13:11 +00:00
|
|
|
|
|
|
|
struct TerrainChunk {
|
|
|
|
// GPU data
|
|
|
|
model: Model<TerrainPipeline>,
|
|
|
|
locals: Consts<TerrainLocals>,
|
2019-06-06 09:04:37 +00:00
|
|
|
visible: bool,
|
|
|
|
z_bounds: (f32, f32),
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
|
2019-01-23 20:01:58 +00:00
|
|
|
struct ChunkMeshState {
|
2019-05-17 17:44:30 +00:00
|
|
|
pos: Vec2<i32>,
|
2019-01-23 20:01:58 +00:00
|
|
|
started_tick: u64,
|
2019-07-04 14:38:55 +00:00
|
|
|
active_worker: Option<u64>,
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
/// A type produced by mesh worker threads corresponding to the position and mesh of a chunk.
|
2019-01-23 20:01:58 +00:00
|
|
|
struct MeshWorkerResponse {
|
2019-05-17 17:44:30 +00:00
|
|
|
pos: Vec2<i32>,
|
2019-06-06 09:04:37 +00:00
|
|
|
z_bounds: (f32, f32),
|
2019-01-23 20:01:58 +00:00
|
|
|
mesh: Mesh<TerrainPipeline>,
|
|
|
|
started_tick: u64,
|
|
|
|
}
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
/// Function executed by worker threads dedicated to chunk meshing.
|
2019-01-23 20:01:58 +00:00
|
|
|
fn mesh_worker(
|
2019-05-17 17:44:30 +00:00
|
|
|
pos: Vec2<i32>,
|
2019-06-06 09:04:37 +00:00
|
|
|
z_bounds: (f32, f32),
|
2019-01-23 20:01:58 +00:00
|
|
|
started_tick: u64,
|
2019-05-17 17:44:30 +00:00
|
|
|
volume: <TerrainMap as SampleVol<Aabr<i32>>>::Sample,
|
|
|
|
range: Aabb<i32>,
|
2019-01-23 20:01:58 +00:00
|
|
|
) -> MeshWorkerResponse {
|
|
|
|
MeshWorkerResponse {
|
|
|
|
pos,
|
2019-06-06 09:04:37 +00:00
|
|
|
z_bounds,
|
2019-05-17 17:44:30 +00:00
|
|
|
mesh: volume.generate_mesh(range),
|
2019-01-23 20:01:58 +00:00
|
|
|
started_tick,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-15 15:13:11 +00:00
|
|
|
pub struct Terrain {
|
2019-07-04 16:33:59 +00:00
|
|
|
chunks: FxHashMap<Vec2<i32>, TerrainChunk>,
|
2019-01-23 20:01:58 +00:00
|
|
|
|
|
|
|
// The mpsc sender and receiver used for talking to meshing worker threads.
|
2019-05-17 09:22:32 +00:00
|
|
|
// We keep the sender component for no reason other than to clone it and send it to new workers.
|
2019-01-23 20:01:58 +00:00
|
|
|
mesh_send_tmp: mpsc::Sender<MeshWorkerResponse>,
|
|
|
|
mesh_recv: mpsc::Receiver<MeshWorkerResponse>,
|
2019-07-04 16:33:59 +00:00
|
|
|
mesh_todo: FxHashMap<Vec2<i32>, ChunkMeshState>,
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Terrain {
|
|
|
|
pub fn new() -> Self {
|
2019-01-23 20:01:58 +00:00
|
|
|
// Create a new mpsc (Multiple Produced, Single Consumer) pair for communicating with
|
|
|
|
// worker threads that are meshing chunks.
|
|
|
|
let (send, recv) = mpsc::channel();
|
|
|
|
|
2019-01-15 15:13:11 +00:00
|
|
|
Self {
|
2019-07-04 16:33:59 +00:00
|
|
|
chunks: FxHashMap::default(),
|
2019-01-23 20:01:58 +00:00
|
|
|
mesh_send_tmp: send,
|
|
|
|
mesh_recv: recv,
|
2019-07-04 16:33:59 +00:00
|
|
|
mesh_todo: FxHashMap::default(),
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-23 20:01:58 +00:00
|
|
|
/// Maintain terrain data. To be called once per tick.
|
2019-06-06 09:04:37 +00:00
|
|
|
pub fn maintain(
|
|
|
|
&mut self,
|
|
|
|
renderer: &mut Renderer,
|
|
|
|
client: &Client,
|
|
|
|
focus_pos: Vec3<f32>,
|
|
|
|
loaded_distance: f32,
|
|
|
|
view_mat: Mat4<f32>,
|
|
|
|
proj_mat: Mat4<f32>,
|
|
|
|
) {
|
2019-01-23 20:01:58 +00:00
|
|
|
let current_tick = client.get_tick();
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
// Add any recently created or changed chunks to the list of chunks to be meshed.
|
2019-07-01 13:36:45 +00:00
|
|
|
for (modified, pos) in client
|
2019-04-29 20:37:19 +00:00
|
|
|
.state()
|
2019-07-20 15:37:01 +00:00
|
|
|
.terrain_changes()
|
2019-07-01 13:36:45 +00:00
|
|
|
.modified_chunks
|
2019-04-29 20:37:19 +00:00
|
|
|
.iter()
|
2019-07-01 13:36:45 +00:00
|
|
|
.map(|c| (true, c))
|
2019-07-01 13:38:45 +00:00
|
|
|
.chain(
|
|
|
|
client
|
|
|
|
.state()
|
2019-07-20 15:37:01 +00:00
|
|
|
.terrain_changes()
|
2019-07-01 13:38:45 +00:00
|
|
|
.new_chunks
|
|
|
|
.iter()
|
|
|
|
.map(|c| (false, c)),
|
|
|
|
)
|
2019-01-23 20:01:58 +00:00
|
|
|
{
|
|
|
|
// 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!
|
2019-04-23 14:49:14 +00:00
|
|
|
for i in -1..2 {
|
|
|
|
for j in -1..2 {
|
2019-05-17 17:44:30 +00:00
|
|
|
let pos = pos + Vec2::new(i, j);
|
|
|
|
|
2019-07-01 13:36:45 +00:00
|
|
|
if !self.chunks.contains_key(&pos) || modified {
|
2019-05-17 17:44:30 +00:00
|
|
|
let mut neighbours = true;
|
|
|
|
for i in -1..2 {
|
|
|
|
for j in -1..2 {
|
|
|
|
neighbours &= client
|
|
|
|
.state()
|
|
|
|
.terrain()
|
|
|
|
.get_key(pos + Vec2::new(i, j))
|
|
|
|
.is_some();
|
2019-05-13 12:08:17 +00:00
|
|
|
}
|
2019-05-17 17:44:30 +00:00
|
|
|
}
|
2019-05-13 12:08:17 +00:00
|
|
|
|
2019-05-17 17:44:30 +00:00
|
|
|
if neighbours {
|
2019-07-01 13:38:45 +00:00
|
|
|
self.mesh_todo.insert(
|
2019-05-17 17:44:30 +00:00
|
|
|
pos,
|
2019-07-01 13:38:45 +00:00
|
|
|
ChunkMeshState {
|
|
|
|
pos,
|
|
|
|
started_tick: current_tick,
|
2019-07-04 14:38:55 +00:00
|
|
|
active_worker: None,
|
2019-07-01 13:38:45 +00:00
|
|
|
},
|
|
|
|
);
|
2019-04-23 14:49:14 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
|
|
|
}
|
2019-07-20 15:37:01 +00:00
|
|
|
|
|
|
|
// Add the chunks belonging to recently changed blocks to the list of chunks to be meshed
|
|
|
|
for pos in client
|
|
|
|
.state()
|
|
|
|
.terrain_changes()
|
|
|
|
.modified_blocks
|
|
|
|
.iter()
|
|
|
|
.map(|(p, _)| *p)
|
|
|
|
{
|
|
|
|
let chunk_pos = client.state().terrain().pos_key(pos);
|
|
|
|
|
|
|
|
self.mesh_todo.insert(
|
|
|
|
chunk_pos,
|
|
|
|
ChunkMeshState {
|
|
|
|
pos: chunk_pos,
|
|
|
|
started_tick: current_tick,
|
|
|
|
active_worker: None,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
// Handle chunks on chunk borders
|
|
|
|
for x in -1..2 {
|
|
|
|
for y in -1..2 {
|
|
|
|
let neighbour_pos = pos + Vec3::new(x, y, 0);
|
|
|
|
let neighbour_chunk_pos = client.state().terrain().pos_key(neighbour_pos);
|
|
|
|
|
|
|
|
if neighbour_chunk_pos != chunk_pos {
|
|
|
|
self.mesh_todo.insert(
|
|
|
|
neighbour_chunk_pos,
|
|
|
|
ChunkMeshState {
|
|
|
|
pos: neighbour_chunk_pos,
|
|
|
|
started_tick: current_tick,
|
|
|
|
active_worker: None,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
// Remove any models for chunks that have been recently removed.
|
2019-07-20 15:37:01 +00:00
|
|
|
for pos in &client.state().terrain_changes().removed_chunks {
|
2019-01-23 20:01:58 +00:00
|
|
|
self.chunks.remove(pos);
|
2019-05-10 19:32:42 +00:00
|
|
|
self.mesh_todo.remove(pos);
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
|
|
|
|
2019-07-04 17:13:29 +00:00
|
|
|
for todo in self
|
|
|
|
.mesh_todo
|
2019-05-10 18:20:14 +00:00
|
|
|
.values_mut()
|
2019-07-04 14:38:55 +00:00
|
|
|
.filter(|todo| {
|
|
|
|
todo.active_worker
|
|
|
|
.map(|worker_tick| worker_tick < todo.started_tick)
|
|
|
|
.unwrap_or(true)
|
|
|
|
})
|
2019-07-04 18:03:44 +00:00
|
|
|
.min_by_key(|todo| todo.active_worker.unwrap_or(todo.started_tick))
|
2019-05-10 20:17:46 +00:00
|
|
|
{
|
2019-07-12 18:51:22 +00:00
|
|
|
if client.thread_pool().queued_jobs() > 0 {
|
2019-07-04 14:38:55 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2019-05-10 20:17:46 +00:00
|
|
|
// Find the area of the terrain we want. Because meshing needs to compute things like
|
2019-05-17 09:22:32 +00:00
|
|
|
// ambient occlusion and edge elision, we also need the borders of the chunk's
|
2019-05-10 20:17:46 +00:00
|
|
|
// neighbours too (hence the `- 1` and `+ 1`).
|
2019-05-17 17:44:30 +00:00
|
|
|
let aabr = Aabr {
|
2019-05-10 20:17:46 +00:00
|
|
|
min: todo
|
|
|
|
.pos
|
|
|
|
.map2(TerrainMap::chunk_size(), |e, sz| e * sz as i32 - 1),
|
|
|
|
max: todo
|
|
|
|
.pos
|
|
|
|
.map2(TerrainMap::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
|
2019-05-17 09:22:32 +00:00
|
|
|
// sample of the terrain that includes both the chunk we want and its neighbours.
|
2019-05-17 17:44:30 +00:00
|
|
|
let volume = match client.state().terrain().sample(aabr) {
|
2019-05-10 20:17:46 +00:00
|
|
|
Ok(sample) => sample,
|
2019-05-17 09:22:32 +00:00
|
|
|
// 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.
|
2019-05-17 17:44:30 +00:00
|
|
|
Err(VolMap2dErr::NoSuchChunk) => return,
|
2019-05-10 20:17:46 +00:00
|
|
|
_ => panic!("Unhandled edge case"),
|
|
|
|
};
|
|
|
|
|
2019-05-17 21:19:32 +00:00
|
|
|
// The region to actually mesh
|
2019-06-04 17:19:40 +00:00
|
|
|
let min_z = volume
|
2019-05-17 21:19:32 +00:00
|
|
|
.iter()
|
2019-06-04 17:19:40 +00:00
|
|
|
.fold(i32::MAX, |min, (_, chunk)| chunk.get_min_z().min(min));
|
|
|
|
let max_z = volume
|
2019-05-17 21:19:32 +00:00
|
|
|
.iter()
|
2019-06-04 17:19:40 +00:00
|
|
|
.fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
|
2019-05-17 21:19:32 +00:00
|
|
|
|
|
|
|
let aabb = Aabb {
|
2019-06-04 17:19:40 +00:00
|
|
|
min: Vec3::from(aabr.min) + Vec3::unit_z() * (min_z - 1),
|
|
|
|
max: Vec3::from(aabr.max) + Vec3::unit_z() * (max_z + 1),
|
2019-05-17 21:19:32 +00:00
|
|
|
};
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
// Clone various things so that they can be moved into the thread.
|
2019-05-10 20:17:46 +00:00
|
|
|
let send = self.mesh_send_tmp.clone();
|
|
|
|
let pos = todo.pos;
|
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
// Queue the worker thread.
|
2019-07-04 14:38:55 +00:00
|
|
|
let started_tick = todo.started_tick;
|
2019-05-10 20:17:46 +00:00
|
|
|
client.thread_pool().execute(move || {
|
2019-06-06 09:04:37 +00:00
|
|
|
let _ = send.send(mesh_worker(
|
|
|
|
pos,
|
|
|
|
(min_z as f32, max_z as f32),
|
2019-07-04 14:38:55 +00:00
|
|
|
started_tick,
|
2019-06-06 09:04:37 +00:00
|
|
|
volume,
|
|
|
|
aabb,
|
|
|
|
));
|
2019-01-23 20:01:58 +00:00
|
|
|
});
|
2019-07-04 14:38:55 +00:00
|
|
|
todo.active_worker = Some(todo.started_tick);
|
2019-05-10 20:17:46 +00:00
|
|
|
}
|
2019-01-23 20:01:58 +00:00
|
|
|
|
2019-05-17 09:22:32 +00:00
|
|
|
// Receive a chunk mesh from a worker thread and upload it to the GPU, then store it.
|
2019-04-25 16:52:08 +00:00
|
|
|
// Only pull out one chunk per frame to avoid an unacceptable amount of blocking lag due
|
|
|
|
// to the GPU upload. That still gives us a 60 chunks / second budget to play with.
|
|
|
|
if let Ok(response) = self.mesh_recv.recv_timeout(Duration::new(0, 0)) {
|
2019-05-10 18:20:14 +00:00
|
|
|
match self.mesh_todo.get(&response.pos) {
|
2019-01-23 20:01:58 +00:00
|
|
|
// It's the mesh we want, insert the newly finished model into the terrain model
|
2019-05-17 09:22:32 +00:00
|
|
|
// data structure (convert the mesh to a model first of course).
|
2019-07-04 14:38:55 +00:00
|
|
|
Some(todo) if response.started_tick <= todo.started_tick => {
|
2019-04-29 20:37:19 +00:00
|
|
|
self.chunks.insert(
|
|
|
|
response.pos,
|
|
|
|
TerrainChunk {
|
|
|
|
model: renderer
|
|
|
|
.create_model(&response.mesh)
|
2019-05-17 09:22:32 +00:00
|
|
|
.expect("Failed to upload chunk mesh to the GPU!"),
|
2019-04-29 20:37:19 +00:00
|
|
|
locals: renderer
|
|
|
|
.create_consts(&[TerrainLocals {
|
2019-05-17 17:54:56 +00:00
|
|
|
model_offs: Vec3::from(
|
|
|
|
response.pos.map2(TerrainMap::chunk_size(), |e, sz| {
|
2019-04-29 20:37:19 +00:00
|
|
|
e as f32 * sz as f32
|
2019-05-17 17:54:56 +00:00
|
|
|
}),
|
|
|
|
)
|
|
|
|
.into_array(),
|
2019-04-29 20:37:19 +00:00
|
|
|
}])
|
2019-05-17 09:22:32 +00:00
|
|
|
.expect("Failed to upload chunk locals to the GPU!"),
|
2019-06-06 09:04:37 +00:00
|
|
|
visible: false,
|
|
|
|
z_bounds: response.z_bounds,
|
2019-04-29 20:37:19 +00:00
|
|
|
},
|
|
|
|
);
|
2019-07-01 13:36:45 +00:00
|
|
|
|
2019-07-04 14:38:55 +00:00
|
|
|
if response.started_tick == todo.started_tick {
|
|
|
|
self.mesh_todo.remove(&response.pos);
|
|
|
|
}
|
2019-04-29 20:37:19 +00:00
|
|
|
}
|
2019-01-23 20:01:58 +00:00
|
|
|
// Chunk must have been removed, or it was spawned on an old tick. Drop the mesh
|
2019-05-17 09:22:32 +00:00
|
|
|
// since it's either out of date or no longer needed.
|
2019-04-29 20:37:19 +00:00
|
|
|
_ => {}
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
|
|
|
}
|
2019-01-15 15:13:11 +00:00
|
|
|
|
2019-06-06 09:04:37 +00:00
|
|
|
// Construct view frustum
|
|
|
|
let frustum = Frustum::from_modelview_and_projection(
|
|
|
|
&view_mat.into_col_array(),
|
|
|
|
&proj_mat.into_col_array(),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Update chunk visibility
|
|
|
|
let chunk_sz = TerrainChunkSize::SIZE.x as f32;
|
|
|
|
for (pos, chunk) in &mut self.chunks {
|
|
|
|
let chunk_pos = pos.map(|e| e as f32 * chunk_sz);
|
|
|
|
|
|
|
|
// 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 in_range = Vec2::<f32>::from(focus_pos).distance_squared(nearest_in_chunk)
|
|
|
|
< loaded_distance.powf(2.0);
|
|
|
|
|
|
|
|
// Ensure the chunk is within the view frustrum
|
|
|
|
let chunk_mid = Vec3::new(
|
|
|
|
chunk_pos.x + chunk_sz / 2.0,
|
|
|
|
chunk_pos.y + chunk_sz / 2.0,
|
|
|
|
(chunk.z_bounds.0 + chunk.z_bounds.1) * 0.5,
|
|
|
|
);
|
2019-06-09 17:05:10 +00:00
|
|
|
let chunk_radius = ((chunk.z_bounds.1 - chunk.z_bounds.0) / 2.0)
|
2019-06-06 09:04:37 +00:00
|
|
|
.max(chunk_sz / 2.0)
|
|
|
|
.powf(2.0)
|
|
|
|
.mul(2.0)
|
|
|
|
.sqrt();
|
|
|
|
let in_frustum = frustum.sphere_intersecting(
|
|
|
|
&chunk_mid.x,
|
|
|
|
&chunk_mid.y,
|
|
|
|
&chunk_mid.z,
|
|
|
|
&chunk_radius,
|
2019-06-06 07:13:58 +00:00
|
|
|
);
|
|
|
|
|
2019-06-06 09:04:37 +00:00
|
|
|
chunk.visible = in_range && in_frustum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render(&self, renderer: &mut Renderer, globals: &Consts<Globals>) {
|
2019-06-06 14:48:41 +00:00
|
|
|
for (_pos, chunk) in &self.chunks {
|
2019-06-06 09:04:37 +00:00
|
|
|
if chunk.visible {
|
2019-06-06 07:13:58 +00:00
|
|
|
renderer.render_terrain_chunk(&chunk.model, globals, &chunk.locals);
|
|
|
|
}
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
}
|