veloren/voxygen/src/scene/terrain.rs

232 lines
8.7 KiB
Rust
Raw Normal View History

2019-01-15 15:13:11 +00:00
use crate::{
2019-01-23 20:01:58 +00:00
mesh::Meshable,
render::{Consts, Globals, Mesh, Model, Renderer, TerrainLocals, TerrainPipeline},
2019-01-15 15:13:11 +00:00
};
use client::Client;
use common::{
terrain::{TerrainChunkSize, TerrainMap},
vol::{SampleVol, VolSize},
volumes::vol_map_2d::VolMap2dErr,
};
use std::{collections::HashMap, i32, sync::mpsc, time::Duration};
use vek::*;
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: Vec2<i32>,
2019-01-23 20:01:58 +00:00
started_tick: u64,
active_worker: bool,
}
/// 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 {
pos: Vec2<i32>,
2019-01-23 20:01:58 +00:00
mesh: Mesh<TerrainPipeline>,
started_tick: u64,
}
/// Function executed by worker threads dedicated to chunk meshing.
2019-01-23 20:01:58 +00:00
fn mesh_worker(
pos: Vec2<i32>,
2019-01-23 20:01:58 +00:00
started_tick: u64,
volume: <TerrainMap as SampleVol<Aabr<i32>>>::Sample,
range: Aabb<i32>,
2019-01-23 20:01:58 +00:00
) -> MeshWorkerResponse {
MeshWorkerResponse {
pos,
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 {
chunks: HashMap<Vec2<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 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>,
mesh_todo: HashMap<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 {
chunks: HashMap::new(),
2019-01-23 20:01:58 +00:00
mesh_send_tmp: send,
mesh_recv: recv,
mesh_todo: HashMap::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()
2019-01-23 20:01:58 +00:00
.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!
for i in -1..2 {
for j in -1..2 {
let pos = pos + Vec2::new(i, j);
if !self.chunks.contains_key(&pos) {
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();
}
}
if neighbours {
self.mesh_todo.entry(pos).or_insert(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.
2019-01-23 20:01:58 +00:00
for pos in &client.state().changes().removed_chunks {
self.chunks.remove(pos);
self.mesh_todo.remove(pos);
2019-01-23 20:01:58 +00:00
}
for todo in self
.mesh_todo
.values_mut()
// Only spawn workers for meshing jobs without an active worker already.
2019-01-23 20:01:58 +00:00
.filter(|todo| !todo.active_worker)
{
// 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(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 its neighbours.
let volume = match client.state().terrain().sample(aabr) {
Ok(sample) => sample,
// 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(VolMap2dErr::NoSuchChunk) => return,
_ => panic!("Unhandled edge case"),
};
// The region to actually mesh
2019-06-04 17:19:40 +00:00
let min_z = volume
.iter()
2019-06-04 17:19:40 +00:00
.fold(i32::MAX, |min, (_, chunk)| chunk.get_min_z().min(min));
let max_z = volume
.iter()
2019-06-04 17:19:40 +00:00
.fold(i32::MIN, |max, (_, chunk)| chunk.get_max_z().max(max));
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),
};
// Clone various things so that they can be moved into the thread.
let send = self.mesh_send_tmp.clone();
let pos = todo.pos;
// Queue the worker thread.
client.thread_pool().execute(move || {
let _ = send.send(mesh_worker(pos, current_tick, volume, aabb));
2019-01-23 20:01:58 +00:00
});
todo.active_worker = true;
}
2019-01-23 20:01:58 +00:00
// Receive a chunk mesh from a worker thread and upload it to the GPU, then store it.
// 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)) {
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
// data structure (convert the mesh to a model first of course).
2019-01-23 20:01:58 +00:00
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: Vec3::from(
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!"),
},
);
}
2019-01-23 20:01:58 +00:00
// 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.
_ => {}
2019-01-23 20:01:58 +00:00
}
}
2019-01-15 15:13:11 +00:00
}
pub fn render(
&self,
renderer: &mut Renderer,
globals: &Consts<Globals>,
focus_pos: Vec3<f32>,
loaded_distance: f32,
) {
for (pos, chunk) in &self.chunks {
// Limit focus_pos to chunk bounds
let chunk_pos = pos.map2(TerrainChunkSize::SIZE.into(), |e, sz: u32| {
e as f32 * sz as f32
});
let nearest_in_chunk = Vec2::from(focus_pos).clamped(
chunk_pos,
chunk_pos + Vec2::from(TerrainChunkSize::SIZE).map(|e: u32| e as f32),
);
if Vec2::<f32>::from(focus_pos).distance_squared(nearest_in_chunk)
< loaded_distance.powf(2.0)
{
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
}
}