2019-01-15 15:13:11 +00:00
|
|
|
// Standard
|
2019-01-23 20:01:58 +00:00
|
|
|
use std::{
|
|
|
|
collections::{HashMap, LinkedList},
|
|
|
|
sync::mpsc,
|
|
|
|
time::Duration,
|
|
|
|
};
|
2019-01-15 15:13:11 +00:00
|
|
|
|
|
|
|
// Library
|
|
|
|
use vek::*;
|
|
|
|
|
2019-01-23 20:01:58 +00:00
|
|
|
// Project
|
|
|
|
use client::Client;
|
|
|
|
use common::{
|
|
|
|
terrain::TerrainMap,
|
2019-01-23 22:21:47 +00:00
|
|
|
volumes::vol_map::VolMapErr,
|
2019-01-23 20:01:58 +00:00
|
|
|
vol::SampleVol,
|
|
|
|
};
|
|
|
|
|
2019-01-15 15:13:11 +00:00
|
|
|
// Crate
|
|
|
|
use crate::{
|
|
|
|
render::{
|
|
|
|
Consts,
|
|
|
|
Globals,
|
|
|
|
Mesh,
|
|
|
|
Model,
|
|
|
|
Renderer,
|
|
|
|
TerrainPipeline,
|
|
|
|
TerrainLocals,
|
|
|
|
},
|
2019-01-23 20:01:58 +00:00
|
|
|
mesh::Meshable,
|
2019-01-15 15:13:11 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
struct TerrainChunk {
|
|
|
|
// GPU data
|
|
|
|
model: Model<TerrainPipeline>,
|
|
|
|
locals: Consts<TerrainLocals>,
|
|
|
|
}
|
|
|
|
|
2019-01-23 20:01:58 +00:00
|
|
|
struct ChunkMeshState {
|
|
|
|
pos: Vec3<i32>,
|
|
|
|
started_tick: u64,
|
|
|
|
active_worker: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A type produced by mesh worker threads corresponding to the position and mesh of a chunk
|
|
|
|
struct MeshWorkerResponse {
|
|
|
|
pos: Vec3<i32>,
|
|
|
|
mesh: Mesh<TerrainPipeline>,
|
|
|
|
started_tick: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Function executed by worker threads dedicated to chunk meshing
|
|
|
|
fn mesh_worker(
|
|
|
|
pos: Vec3<i32>,
|
|
|
|
started_tick: u64,
|
|
|
|
volume: <TerrainMap as SampleVol>::Sample,
|
|
|
|
) -> MeshWorkerResponse {
|
|
|
|
MeshWorkerResponse {
|
|
|
|
pos,
|
|
|
|
mesh: volume.generate_mesh(()),
|
|
|
|
started_tick,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-15 15:13:11 +00:00
|
|
|
pub struct Terrain {
|
|
|
|
chunks: HashMap<Vec3<i32>, TerrainChunk>,
|
2019-01-23 20:01:58 +00:00
|
|
|
|
|
|
|
// The mpsc sender and receiver used for talking to meshing worker threads.
|
|
|
|
// We keep the sender component for no reason othe than to clone it and send it to new workers.
|
|
|
|
mesh_send_tmp: mpsc::Sender<MeshWorkerResponse>,
|
|
|
|
mesh_recv: mpsc::Receiver<MeshWorkerResponse>,
|
|
|
|
mesh_todo: LinkedList<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 {
|
|
|
|
chunks: HashMap::new(),
|
2019-01-23 20:01:58 +00:00
|
|
|
|
|
|
|
mesh_send_tmp: send,
|
|
|
|
mesh_recv: recv,
|
|
|
|
mesh_todo: LinkedList::new(),
|
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.
|
|
|
|
pub fn maintain(&mut self, renderer: &mut Renderer, client: &Client) {
|
|
|
|
let current_tick = client.get_tick();
|
|
|
|
|
|
|
|
// Add any recently created or changed chunks to the list of chunks to be meshed
|
|
|
|
for pos in client.state().changes().new_chunks.iter()
|
|
|
|
.chain(client.state().changes().changed_chunks.iter())
|
|
|
|
{
|
|
|
|
// 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 {
|
|
|
|
for k in -1..2 {
|
|
|
|
let pos = pos + Vec3::new(i, j, k);
|
|
|
|
|
|
|
|
match self.mesh_todo.iter_mut().find(|todo| todo.pos == pos) {
|
|
|
|
Some(todo) => todo.started_tick = current_tick,
|
|
|
|
// The chunk it's queued yet, add it to the queue
|
|
|
|
None => self.mesh_todo.push_back(ChunkMeshState {
|
|
|
|
pos,
|
|
|
|
started_tick: current_tick,
|
|
|
|
active_worker: false,
|
|
|
|
}),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-01-23 20:01:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove any models for chunks that have been recently removed
|
|
|
|
for pos in &client.state().changes().removed_chunks {
|
|
|
|
self.chunks.remove(pos);
|
|
|
|
self.mesh_todo.drain_filter(|todo| todo.pos == *pos);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clone the sender to the thread can send us the chunk data back
|
|
|
|
// TODO: It's a bit hacky cloning it here and then cloning it again below. Fix this.
|
|
|
|
let send = self.mesh_send_tmp.clone();
|
|
|
|
|
|
|
|
self.mesh_todo
|
|
|
|
.iter_mut()
|
|
|
|
// Only spawn workers for meshing jobs without an active worker already
|
|
|
|
.filter(|todo| !todo.active_worker)
|
|
|
|
.for_each(|todo| {
|
|
|
|
// Find the area of the terrain we want. Because meshing needs to compute things like
|
|
|
|
// ambient occlusion and edge elision, we also need to borders of the chunk's
|
|
|
|
// neighbours too (hence the `- 1` and `+ 1`).
|
|
|
|
let aabb = Aabb {
|
|
|
|
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
|
|
|
|
// sample of the terrain that includes both the chunk we want and
|
|
|
|
let volume = match client.state().terrain().sample(aabb) {
|
|
|
|
Ok(sample) => sample,
|
|
|
|
// If either this chunk or its neighbours doesn't yet exist, so we keep it in the
|
|
|
|
// todo queue to be processed at a later date when we have its neighbours.
|
|
|
|
Err(VolMapErr::NoSuchChunk) => return,
|
|
|
|
_ => panic!("Unhandled edge case"),
|
|
|
|
};
|
|
|
|
|
|
|
|
// Clone various things to that they can be moved into the thread
|
|
|
|
let send = send.clone();
|
|
|
|
let pos = todo.pos;
|
|
|
|
|
|
|
|
// Queue the worker thread
|
|
|
|
client.thread_pool().execute(move || {
|
|
|
|
send.send(mesh_worker(pos, current_tick, volume))
|
|
|
|
.expect("Failed to send chunk mesh to main thread");
|
|
|
|
});
|
|
|
|
todo.active_worker = true;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Receive chunk meshes from worker threads, upload them to the GPU and then store them
|
|
|
|
while let Ok(response) = self.mesh_recv.recv_timeout(Duration::new(0, 0)) {
|
|
|
|
match self.mesh_todo.iter().find(|todo| todo.pos == 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 => {
|
|
|
|
self.chunks.insert(response.pos, TerrainChunk {
|
|
|
|
model: renderer.create_model(&response.mesh).expect("Failed to upload chunk mesh to the GPU"),
|
|
|
|
locals: renderer.create_consts(&[TerrainLocals {
|
|
|
|
model_offs: response.pos.map2(TerrainMap::chunk_size(), |e, sz| e as f32 * sz as f32).into_array(),
|
|
|
|
}]).expect("Failed to upload chunk locals to the GPU"),
|
|
|
|
});
|
|
|
|
},
|
|
|
|
// 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
|
|
|
|
_ => continue,
|
|
|
|
}
|
|
|
|
}
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn render(&self, renderer: &mut Renderer, globals: &Consts<Globals>) {
|
2019-01-23 22:21:47 +00:00
|
|
|
for (_, chunk) in &self.chunks {
|
2019-01-23 20:01:58 +00:00
|
|
|
renderer.render_terrain_chunk(
|
|
|
|
&chunk.model,
|
|
|
|
globals,
|
|
|
|
&chunk.locals,
|
|
|
|
);
|
|
|
|
}
|
2019-01-15 15:13:11 +00:00
|
|
|
}
|
|
|
|
}
|