Implement chunk unloading, frustrum culling and some other minor optimisations
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Sijmen 2021-06-03 16:34:42 +02:00
parent 206f7857fa
commit 4bba01058b
Signed by: vijfhoek
GPG key ID: 82D05C89B28B0DAE
12 changed files with 230 additions and 120 deletions

1
.gitignore vendored
View file

@ -4,5 +4,6 @@
/perf.data*
/chunks/
GPUCache/
profile.txt
callgrind.out.*

View file

@ -1,49 +1,23 @@
use cgmath::Vector3;
use std::ops::{Div, Sub};
use cgmath::Point3;
pub struct Aabb<T> {
pub min: Vector3<T>,
pub max: Vector3<T>,
pub struct Aabb {
pub min: Point3<f32>,
pub max: Point3<f32>,
}
impl<T: Ord + Copy + Sub<Output = T> + Div<Output = T>> Aabb<T> {
pub fn intersects_ray(
&self,
ray_origin: Vector3<T>,
ray_direction: Vector3<T>,
) -> 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),
}
}
}

View file

@ -1,7 +1,5 @@
use cgmath::{Matrix4, Point3, Rad, Vector3};
use crate::aabb::Aabb;
#[rustfmt::skip]
pub const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
1.0, 0.0, 0.0, 0.0,
@ -10,11 +8,6 @@ pub const OPENGL_TO_WGPU_MATRIX: Matrix4<f32> = Matrix4::new(
0.0, 0.0, 0.5, 1.0,
);
const AABB: Aabb<f32> = 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<f32>,
pub yaw: Rad<f32>,

View file

@ -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<Block>; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE];
#[derive(Clone, Default)]
pub struct Chunk {
pub blocks: ChunkBlocks,
pub buffers: Option<GeometryBuffers<u16>>,
}
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<isize>, 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)
}
}

View file

@ -77,7 +77,7 @@ impl<I: bytemuck::Pod> GeometryBuffers<I> {
}
impl GeometryBuffers<u16> {
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);
}

View file

@ -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::<SwapChainError>() {
Err(root_cause) => match root_cause.downcast_ref::<SwapChainError>() {
// 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,
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 => {

View file

@ -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) {

View file

@ -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);

View file

@ -28,7 +28,7 @@ pub const PRIMITIVE_STATE: wgpu::PrimitiveState = wgpu::PrimitiveState {
pub struct State {
pub window_size: PhysicalSize<u32>,
render_context: RenderContext,
world_state: WorldState,
pub world_state: WorldState,
hud_state: HudState,
pub mouse_grabbed: bool,

View file

@ -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(

View file

@ -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<f32>,
view_projection: Matrix4<f32>,
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],
}

View file

@ -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<Point3<isize>, Chunk>,
pub npc: Npc,
pub chunks: AHashMap<Point3<isize>, Chunk>,
pub chunk_database: sled::Db,
pub chunk_save_queue: VecDeque<Point3<isize>>,
pub chunk_save_queue: VecDeque<(Point3<isize>, bool)>,
pub chunk_load_queue: VecDeque<Point3<isize>>,
pub chunk_generate_queue: VecDeque<Point3<isize>>,
pub chunk_buffers: AHashMap<Point3<isize>, GeometryBuffers<u16>>,
pub highlighted: Option<(Point3<isize>, Vector3<i32>)>,
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();
}
} 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,23 +120,43 @@ 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<f32> = (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);
if let Some(buffers) = chunk.buffers.as_ref() {
buffers.apply_buffers(render_pass);
triangle_count += buffers.draw_indexed(render_pass);
}
}
{
let buffers = self.npc.geometry_buffers.as_ref().unwrap();
@ -129,21 +167,35 @@ impl World {
triangle_count
}
pub fn enqueue_chunk_save(&mut self, position: Point3<isize>, 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<isize>,
) {
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<f32>, scalar: f32) -> f32 {