From 4bba01058bc6cb89791b14bcbecfcb1b7d5e3324 Mon Sep 17 00:00:00 2001 From: Vijfhoek Date: Thu, 3 Jun 2021 16:34:42 +0200 Subject: [PATCH] Implement chunk unloading, frustrum culling and some other minor optimisations --- .gitignore | 1 + src/aabb.rs | 62 +++++++--------------- src/camera.rs | 7 --- src/chunk.rs | 32 +++++++++-- src/geometry.rs | 2 +- src/main.rs | 37 ++++++++----- src/npc.rs | 4 +- src/state/hud_state.rs | 6 +-- src/state/mod.rs | 2 +- src/state/world_state.rs | 16 +++--- src/view.rs | 69 ++++++++++++++++++++---- src/world.rs | 112 ++++++++++++++++++++++++++++----------- 12 files changed, 230 insertions(+), 120 deletions(-) diff --git a/.gitignore b/.gitignore index b2b5659..1b30a73 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ /perf.data* /chunks/ +GPUCache/ profile.txt callgrind.out.* diff --git a/src/aabb.rs b/src/aabb.rs index c7589db..d78ba82 100644 --- a/src/aabb.rs +++ b/src/aabb.rs @@ -1,49 +1,23 @@ -use cgmath::Vector3; -use std::ops::{Div, Sub}; +use cgmath::Point3; -pub struct Aabb { - pub min: Vector3, - pub max: Vector3, +pub struct Aabb { + pub min: Point3, + pub max: Point3, } -impl + Div> Aabb { - pub fn intersects_ray( - &self, - ray_origin: Vector3, - ray_direction: Vector3, - ) -> Option<(T, T)> { - let mut t_min = (self.min.x - ray_origin.x) / ray_direction.x; - let mut t_max = (self.max.x - ray_origin.x) / ray_direction.x; - if t_min > t_max { - std::mem::swap(&mut t_min, &mut t_max); - } - - let mut ty_min = (self.min.y - ray_origin.y) / ray_direction.y; - let mut ty_max = (self.max.y - ray_origin.y) / ray_direction.y; - if ty_min > ty_max { - std::mem::swap(&mut ty_min, &mut ty_max); - } - - if t_min > ty_max || ty_min > t_max { - return None; - } - - t_min = T::max(t_min, ty_min); - t_max = T::min(t_min, ty_max); - - let mut tz_min = (self.min.z - ray_origin.z) / ray_direction.z; - let mut tz_max = (self.max.z - ray_origin.z) / ray_direction.z; - if tz_min > tz_max { - std::mem::swap(&mut tz_min, &mut tz_max); - } - - if t_min > tz_max || tz_min > t_max { - return None; - } - - t_min = T::max(t_min, tz_min); - t_max = T::max(t_max, tz_max); - - Some((t_min, t_max)) +impl Aabb { + pub fn intersects(&self, other: &Self) -> bool { + (self.min.x <= other.max.x && self.max.x >= other.min.x) + && (self.min.y <= other.max.y && self.max.y >= other.min.y) + && (self.min.z <= other.max.z && self.max.z >= other.min.z) + } +} + +impl Default for Aabb { + fn default() -> Self { + Self { + min: Point3::new(0.0, 0.0, 0.0), + max: Point3::new(0.0, 0.0, 0.0), + } } } diff --git a/src/camera.rs b/src/camera.rs index 7ad3d98..7d48629 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,7 +1,5 @@ use cgmath::{Matrix4, Point3, Rad, Vector3}; -use crate::aabb::Aabb; - #[rustfmt::skip] pub const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( 1.0, 0.0, 0.0, 0.0, @@ -10,11 +8,6 @@ pub const OPENGL_TO_WGPU_MATRIX: Matrix4 = Matrix4::new( 0.0, 0.0, 0.5, 1.0, ); -const AABB: Aabb = Aabb { - min: Vector3::new(-0.3, 1.62, -0.3), - max: Vector3::new(0.3, 1.8 - 1.62, 0.3), -}; - pub struct Camera { pub position: Point3, pub yaw: Rad, diff --git a/src/chunk.rs b/src/chunk.rs index 9a50bd5..f3c6f5a 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,6 +1,12 @@ use std::{collections::VecDeque, usize}; -use crate::{geometry::Geometry, quad::Quad, vertex::BlockVertex}; +use crate::{ + aabb::Aabb, + geometry::{Geometry, GeometryBuffers}, + quad::Quad, + vertex::BlockVertex, + view::View, +}; use ahash::{AHashMap, AHashSet}; use cgmath::{Point3, Vector3}; use noise::utils::{NoiseMapBuilder, PlaneMapBuilder}; @@ -68,9 +74,18 @@ pub const CHUNK_ISIZE: isize = CHUNK_SIZE as isize; type ChunkBlocks = [[[Option; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]; -#[derive(Clone, Default)] pub struct Chunk { pub blocks: ChunkBlocks, + pub buffers: Option>, +} + +impl Default for Chunk { + fn default() -> Self { + Self { + blocks: [[[None; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE], + buffers: None, + } + } } impl Serialize for Chunk { @@ -179,7 +194,7 @@ impl Chunk { }); } - if dirt_max >= 0 && dirt_max < CHUNK_ISIZE { + if (0..CHUNK_ISIZE).contains(&dirt_max) { self.blocks[dirt_max as usize][z][x] = Some(Block { block_type: BlockType::Grass, }); @@ -410,4 +425,15 @@ impl Chunk { Ok(true) } } + + pub fn is_visible(&self, position: Point3, view: &View) -> bool { + let aabb = Aabb { + min: position.cast().unwrap(), + max: (position + Vector3::new(CHUNK_ISIZE, CHUNK_ISIZE, CHUNK_ISIZE)) + .cast() + .unwrap(), + }; + + aabb.intersects(&view.aabb) + } } diff --git a/src/geometry.rs b/src/geometry.rs index 52b8b7c..8a44da0 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -77,7 +77,7 @@ impl GeometryBuffers { } impl GeometryBuffers { - pub fn set_buffers<'a>(&'a self, render_pass: &mut RenderPass<'a>) { + pub fn apply_buffers<'a>(&'a self, render_pass: &mut RenderPass<'a>) { render_pass.set_vertex_buffer(0, self.vertices.slice(..)); render_pass.set_index_buffer(self.indices.slice(..), wgpu::IndexFormat::Uint16); } diff --git a/src/main.rs b/src/main.rs index cebc99a..bdf4459 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod aabb; mod camera; mod chunk; mod geometry; +mod npc; mod quad; mod render_context; mod state; @@ -11,7 +12,6 @@ mod time; mod vertex; mod view; mod world; -mod npc; use std::time::{Duration, Instant}; use wgpu::SwapChainError; @@ -133,9 +133,21 @@ fn main() { let fps_max = 1_000_000 / frametime_min.as_micros(); let fps_min = 1_000_000 / frametime_max.as_micros(); + print!("{:>4} frames | ", frames); + print!( + "frametime avg={:>5.2}ms min={:>5.2}ms max={:>5.2}ms | ", + frametime.as_secs_f32() * 1000.0, + frametime_min.as_secs_f32() * 1000.0, + frametime_max.as_secs_f32() * 1000.0, + ); + print!( + "fps avg={:>5} min={:>5} max={:>5} | ", + fps, fps_min, fps_max + ); println!( - "{:>4} frames | frametime avg={:>5.2}ms min={:>5.2}ms max={:>5.2}ms | fps avg={:>5} min={:>5} max={:>5} | {:>8} tris", - frames, frametime.as_secs_f32() * 1000.0, frametime_min.as_secs_f32() * 1000.0, frametime_max.as_secs_f32() * 1000.0, fps, fps_min, fps_max, triangle_count, + "{:>8} tris | {:>5} chunks", + triangle_count, + state.world_state.world.chunks.len() ); elapsed = Duration::from_secs(0); @@ -150,17 +162,18 @@ fn main() { state.update(dt); match state.render() { - Err(root_cause) => - match root_cause.downcast_ref::() { - // Recreate the swap_chain if lost - Some(wgpu::SwapChainError::Lost) => state.resize(state.window_size), - // The system is out of memory, we should probably quit - Some(wgpu::SwapChainError::OutOfMemory) => *control_flow = ControlFlow::Exit, - // All other errors (Outdated, Timeout) should be resolved by the next frame - Some(_) | None => eprintln!("{:?}", root_cause), + Err(root_cause) => match root_cause.downcast_ref::() { + // Recreate the swap_chain if lost + Some(wgpu::SwapChainError::Lost) => state.resize(state.window_size), + // The system is out of memory, we should probably quit + Some(wgpu::SwapChainError::OutOfMemory) => { + *control_flow = ControlFlow::Exit } + // All other errors (Outdated, Timeout) should be resolved by the next frame + Some(_) | None => eprintln!("{:?}", root_cause), + }, - Ok(v) => triangle_count = v + Ok(v) => triangle_count = v, } } Event::MainEventsCleared => { diff --git a/src/npc.rs b/src/npc.rs index 546885f..1e24243 100644 --- a/src/npc.rs +++ b/src/npc.rs @@ -55,13 +55,13 @@ impl Npc { } } - return Self { + Self { position, scale, rotation, geometry: Geometry::new(vertices, indices), geometry_buffers: None, - }; + } } pub fn load_geometry(&mut self, render_context: &RenderContext) { diff --git a/src/state/hud_state.rs b/src/state/hud_state.rs index a78881d..33f5d1c 100644 --- a/src/state/hud_state.rs +++ b/src/state/hud_state.rs @@ -124,19 +124,19 @@ impl HudState { render_pass.set_pipeline(&self.render_pipeline); // Render the HUD elements - self.hud_geometry_buffers.set_buffers(&mut render_pass); + self.hud_geometry_buffers.apply_buffers(&mut render_pass); render_pass.set_bind_group(0, &self.texture_bind_group, &[]); self.hud_geometry_buffers.draw_indexed(&mut render_pass); render_pass.draw_indexed(0..self.hud_geometry_buffers.index_count as u32, 0, 0..1); // Render the FPS text - self.fps_geometry_buffers.set_buffers(&mut render_pass); + self.fps_geometry_buffers.apply_buffers(&mut render_pass); render_pass.set_bind_group(0, &self.text_renderer.bind_group, &[]); self.fps_geometry_buffers.draw_indexed(&mut render_pass); // Render the coordinates text self.coordinates_geometry_buffers - .set_buffers(&mut render_pass); + .apply_buffers(&mut render_pass); render_pass.set_bind_group(0, &self.text_renderer.bind_group, &[]); self.coordinates_geometry_buffers .draw_indexed(&mut render_pass); diff --git a/src/state/mod.rs b/src/state/mod.rs index 936ef0f..acbc1c9 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -28,7 +28,7 @@ pub const PRIMITIVE_STATE: wgpu::PrimitiveState = wgpu::PrimitiveState { pub struct State { pub window_size: PhysicalSize, render_context: RenderContext, - world_state: WorldState, + pub world_state: WorldState, hud_state: HudState, pub mouse_grabbed: bool, diff --git a/src/state/world_state.rs b/src/state/world_state.rs index fe509cb..6ef7905 100644 --- a/src/state/world_state.rs +++ b/src/state/world_state.rs @@ -67,7 +67,7 @@ impl WorldState { render_context.swap_chain_descriptor.height, cgmath::Deg(45.0), 0.1, - 5000.0, + 300.0, ); (camera, projection) @@ -85,7 +85,7 @@ impl WorldState { .device .create_buffer_init(&BufferInitDescriptor { label: Some("view_buffer"), - contents: bytemuck::cast_slice(&[view]), + contents: bytemuck::cast_slice(&[view.to_raw()]), usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, }); @@ -324,7 +324,7 @@ impl WorldState { render_pass.set_bind_group(1, &self.view_bind_group, &[]); render_pass.set_bind_group(2, &self.time_bind_group, &[]); - triangle_count += self.world.render(&mut render_pass, &self.camera); + triangle_count += self.world.render(&mut render_pass, &self.view); triangle_count } @@ -428,13 +428,15 @@ impl WorldState { pub fn update(&mut self, dt: Duration, render_context: &RenderContext) { self.update_position(dt); - self.world.update(render_context, &self.camera); + self.world.update(dt, render_context, &self.camera); self.view .update_view_projection(&self.camera, &self.projection); - render_context - .queue - .write_buffer(&self.view_buffer, 0, bytemuck::cast_slice(&[self.view])); + render_context.queue.write_buffer( + &self.view_buffer, + 0, + bytemuck::cast_slice(&[self.view.to_raw()]), + ); self.time.time += dt.as_secs_f32(); render_context.queue.write_buffer( diff --git a/src/view.rs b/src/view.rs index 1593dce..c87ff7f 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,24 +1,73 @@ -use cgmath::SquareMatrix; +use cgmath::{EuclideanSpace, Matrix4, Point3, SquareMatrix, Vector4, Zero}; -use crate::camera::{Camera, Projection}; +use crate::{ + aabb::Aabb, + camera::{Camera, Projection, OPENGL_TO_WGPU_MATRIX}, +}; -#[repr(C)] -#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] pub struct View { - view_position: [f32; 4], - view_projection: [[f32; 4]; 4], + view_position: Vector4, + view_projection: Matrix4, + pub aabb: Aabb, } impl View { + pub fn to_raw(&self) -> ViewRaw { + ViewRaw { + view_position: self.view_position.into(), + view_projection: self.view_projection.into(), + } + } + pub fn new() -> Self { Self { - view_position: [0.0; 4], - view_projection: cgmath::Matrix4::identity().into(), + view_position: Vector4::zero(), + view_projection: Matrix4::identity(), + aabb: Aabb::default(), } } pub fn update_view_projection(&mut self, camera: &Camera, projection: &Projection) { - self.view_position = camera.position.to_homogeneous().into(); - self.view_projection = (projection.calculate_matrix() * camera.calculate_matrix()).into(); + self.view_position = camera.position.to_homogeneous(); + self.view_projection = projection.calculate_matrix() * camera.calculate_matrix(); + self.aabb = self.frustrum_aabb(); + } + + fn frustrum_aabb(&self) -> Aabb { + let projection = OPENGL_TO_WGPU_MATRIX.invert().unwrap() * self.view_projection; + let inverse_matrix = projection.invert().unwrap(); + + let corners = &[ + Vector4::new(-1.0, -1.0, 1.0, 1.0), + Vector4::new(-1.0, -1.0, -1.0, 1.0), + Vector4::new(-1.0, 1.0, 1.0, 1.0), + Vector4::new(-1.0, 1.0, -1.0, 1.0), + Vector4::new(1.0, -1.0, 1.0, 1.0), + Vector4::new(1.0, -1.0, -1.0, 1.0), + Vector4::new(1.0, 1.0, 1.0, 1.0), + Vector4::new(1.0, 1.0, -1.0, 1.0), + ]; + + let mut min = Vector4::new(f32::INFINITY, f32::INFINITY, f32::INFINITY, 1.0); + let mut max = Vector4::new(0.0, 0.0, 0.0, 1.0); + for &corner in corners { + let corner = inverse_matrix * corner; + let corner = corner / corner.w; + + min = min.zip(corner, f32::min); + max = max.zip(corner, f32::max); + } + + Aabb { + min: Point3::from_vec(min.truncate()), + max: Point3::from_vec(max.truncate()), + } } } + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct ViewRaw { + view_position: [f32; 4], + view_projection: [[f32; 4]; 4], +} diff --git a/src/world.rs b/src/world.rs index 1ef3915..6874c54 100644 --- a/src/world.rs +++ b/src/world.rs @@ -1,4 +1,4 @@ -use std::collections::{HashMap, VecDeque}; +use std::{collections::VecDeque, time::Duration}; use crate::{ camera::Camera, @@ -6,30 +6,34 @@ use crate::{ geometry::GeometryBuffers, npc::Npc, render_context::RenderContext, + view::View, }; use ahash::AHashMap; -use cgmath::{EuclideanSpace, InnerSpace, Point3, Vector2, Vector3}; +use cgmath::{EuclideanSpace, InnerSpace, Point3, Vector3}; use wgpu::{BufferUsage, RenderPass}; pub struct World { - pub chunks: HashMap, Chunk>, pub npc: Npc, + pub chunks: AHashMap, Chunk>, pub chunk_database: sled::Db, - pub chunk_save_queue: VecDeque>, + pub chunk_save_queue: VecDeque<(Point3, bool)>, pub chunk_load_queue: VecDeque>, pub chunk_generate_queue: VecDeque>, - pub chunk_buffers: AHashMap, GeometryBuffers>, pub highlighted: Option<(Point3, Vector3)>, + + pub unload_timer: Duration, } pub const RENDER_DISTANCE: isize = 8; pub const WORLD_HEIGHT: isize = 16 * 16 / CHUNK_ISIZE; +const DEBUG_IO: bool = false; + impl World { pub fn new() -> Self { - let chunks = HashMap::new(); + let chunks = AHashMap::new(); let npc = Npc::load(); let chunk_database = sled::Config::new() @@ -47,13 +51,14 @@ impl World { chunk_load_queue: VecDeque::new(), chunk_save_queue: VecDeque::new(), chunk_generate_queue: VecDeque::new(), - chunk_buffers: AHashMap::new(), highlighted: None, + + unload_timer: Duration::new(0, 0), } } - pub fn update(&mut self, render_context: &RenderContext, camera: &Camera) { + pub fn update(&mut self, dt: Duration, render_context: &RenderContext, camera: &Camera) { if let Some(position) = self.chunk_load_queue.pop_front() { let chunk = self.chunks.entry(position).or_default(); match chunk.load(position, &self.chunk_database) { @@ -62,20 +67,33 @@ impl World { } Ok(true) => { self.update_chunk_geometry(render_context, position); - self.chunk_save_queue.push_back(position); - // println!("Generated chunk {:?}", position); + self.enqueue_chunk_save(position, false); + if DEBUG_IO { + println!("Generated chunk {:?}", position); + } } Ok(false) => { self.update_chunk_geometry(render_context, position); - // println!("Loaded chunk {:?}", position); + if DEBUG_IO { + println!("Loaded chunk {:?}", position); + } } } - } else if let Some(position) = self.chunk_save_queue.pop_front() { - let chunk = self.chunks.get(&position).unwrap(); - if let Err(err) = chunk.save(position, &self.chunk_database) { - eprintln!("Failed to save chunk {:?}: {:?}", position, err); + } else if let Some((position, unload)) = self.chunk_save_queue.pop_front() { + if let Some(chunk) = self.chunks.get(&position) { + if let Err(err) = chunk.save(position, &self.chunk_database) { + eprintln!("Failed to save chunk {:?}: {:?}", position, err); + } else if unload { + self.chunks.remove(&position); + + if DEBUG_IO { + println!("Saved and unloaded chunk {:?}", position); + } + } else if DEBUG_IO { + println!("Saved chunk {:?}", position); + } } else { - // println!("Saved chunk {:?}", position); + eprintln!("Tried to save unloaded chunk {:?}", position); } } @@ -102,22 +120,42 @@ impl World { }); self.chunk_load_queue.extend(load_queue); + + // Unload chunks that are far away + self.unload_timer += dt; + if self.unload_timer.as_secs() >= 10 { + self.unload_timer = Duration::new(0, 0); + + let camera_pos = camera.position.to_vec(); + let unload_distance = (RENDER_DISTANCE * CHUNK_ISIZE) as f32 * 1.5; + + let mut unload_chunks = Vec::new(); + for point in self.chunks.keys() { + let pos: Point3 = (point * CHUNK_ISIZE).cast().unwrap(); + if (pos.x - camera_pos.x).abs() > unload_distance + || (pos.z - camera_pos.z).abs() > unload_distance + { + unload_chunks.push(*point); + } + } + for point in unload_chunks { + self.enqueue_chunk_save(point, true); + } + } } - pub fn render<'a>(&'a self, render_pass: &mut RenderPass<'a>, camera: &Camera) -> usize { - let camera_pos = camera.position.to_vec(); - let camera_pos = Vector2::new(camera_pos.x, camera_pos.z); + pub fn render<'a>(&'a self, render_pass: &mut RenderPass<'a>, view: &View) -> usize { let mut triangle_count = 0; - for (position, buffers) in &self.chunk_buffers { - let pos = (position * CHUNK_ISIZE).cast().unwrap(); - let pos = Vector2::new(pos.x, pos.z); - if (pos - camera_pos).magnitude() > 300.0 { + for (position, chunk) in &self.chunks { + if !chunk.is_visible(position * CHUNK_ISIZE, &view) { continue; } - buffers.set_buffers(render_pass); - triangle_count += buffers.draw_indexed(render_pass); + if let Some(buffers) = chunk.buffers.as_ref() { + buffers.apply_buffers(render_pass); + triangle_count += buffers.draw_indexed(render_pass); + } } { @@ -129,21 +167,35 @@ impl World { triangle_count } + pub fn enqueue_chunk_save(&mut self, position: Point3, unload: bool) { + if let Some((_, unload_)) = self + .chunk_save_queue + .iter_mut() + .find(|(pos, _)| pos == &position) + { + *unload_ = *unload_ || unload; + } else { + self.chunk_save_queue.push_back((position, unload)); + } + } + pub fn update_chunk_geometry( &mut self, render_context: &RenderContext, chunk_position: Point3, ) { - let chunk = &mut self.chunks.get(&chunk_position).unwrap(); + let chunk = self.chunks.get_mut(&chunk_position).unwrap(); let offset = chunk_position * CHUNK_ISIZE; let geometry = chunk.to_geometry( offset, World::highlighted_for_chunk(self.highlighted, &chunk_position).as_ref(), ); - let buffers = - GeometryBuffers::from_geometry(render_context, &geometry, BufferUsage::empty()); - self.chunk_buffers.insert(chunk_position, buffers); + chunk.buffers = Some(GeometryBuffers::from_geometry( + render_context, + &geometry, + BufferUsage::empty(), + )); } fn update_highlight(&mut self, render_context: &RenderContext, camera: &Camera) { @@ -247,7 +299,7 @@ impl World { } self.chunk_save_queue - .push_back(chunk_position / CHUNK_ISIZE); + .push_back((chunk_position / CHUNK_ISIZE, false)); } fn calc_scale(vector: Vector3, scalar: f32) -> f32 {