diff --git a/.gitignore b/.gitignore index 6985cf1..07db1ba 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + +#old source +_src diff --git a/Cargo.toml b/Cargo.toml index 20a81de..769fdad 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,6 @@ name = "kubi" version = "0.1.0" edition = "2021" -[profile.dev.package.noise] -opt-level = 3 - [dependencies] glium = "0.32" image = { version = "0.24", default_features = false, features = ["png"] } @@ -14,9 +11,10 @@ env_logger = "0.10" strum = { version = "0.24", features = ["derive"] } glam = { version = "0.22", features = ["debug-glam-assert", "mint", "fast-math"] } hashbrown = "0.13" -noise = "0.8" rayon = "1.6" -#ordered-float = "3.4" - -[features] -default = [] +shipyard = { version = "0.6", features = ["thread_local"] } +nohash-hasher = "0.2.0" +anyhow = "1.0" +flume = "0.10" +#once_cell = "1.17" +bracket-noise = "0.8" diff --git a/README.md b/README.md index c5b08c0..928969d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ -# development moved to -# https://github.com/griffi-gh/kubi/tree/ecs-rewrite -this branch contains the latest version before the rewrite +

Kubi

+work in progress +
~ uwu
\ No newline at end of file diff --git a/crabs.txt b/crabs.txt new file mode 100644 index 0000000..aecc062 --- /dev/null +++ b/crabs.txt @@ -0,0 +1 @@ +sorry no crabs here diff --git a/src/game/shaders/glsl/colored2d.frag b/shaders/selection_box.frag similarity index 55% rename from src/game/shaders/glsl/colored2d.frag rename to shaders/selection_box.frag index 9b2ac94..42b54ed 100644 --- a/src/game/shaders/glsl/colored2d.frag +++ b/shaders/selection_box.frag @@ -5,4 +5,5 @@ uniform vec4 u_color; void main() { color = u_color; + color -= vec4(0, 0, 0, 0.1 * sin(gl_FragCoord.x) * cos(gl_FragCoord.y)); } diff --git a/shaders/selection_box.vert b/shaders/selection_box.vert new file mode 100644 index 0000000..7243a1f --- /dev/null +++ b/shaders/selection_box.vert @@ -0,0 +1,10 @@ +#version 150 core + +in vec3 position; +uniform vec3 u_position; +uniform mat4 perspective; +uniform mat4 view; + +void main() { + gl_Position = perspective * view * vec4(position + u_position, 1.); +} diff --git a/src/game/shaders/glsl/chunk.frag b/shaders/world.frag similarity index 100% rename from src/game/shaders/glsl/chunk.frag rename to shaders/world.frag diff --git a/src/game/shaders/glsl/chunk.vert b/shaders/world.vert similarity index 52% rename from src/game/shaders/glsl/chunk.vert rename to shaders/world.vert index 91e2b39..de3d2e7 100644 --- a/src/game/shaders/glsl/chunk.vert +++ b/shaders/world.vert @@ -1,5 +1,11 @@ #version 150 core +//TODO pack this data: +// uint position_normal_uv +// XXYYZZNU +// wehere Normal and Uv are enums +// maybe somehow pack in tex index too + in vec3 position; in vec3 normal; in vec2 uv; @@ -7,7 +13,7 @@ in uint tex_index; out vec2 v_uv; out vec3 v_normal; flat out uint v_tex_index; -uniform vec2 position_offset; +uniform vec3 position_offset; uniform mat4 perspective; uniform mat4 view; @@ -15,5 +21,5 @@ void main() { v_normal = normal; v_tex_index = tex_index; v_uv = uv; - gl_Position = perspective * view * (vec4(position, 1.0) + vec4(position_offset.x, 0., position_offset.y, 0.)); + gl_Position = perspective * view * vec4(position + position_offset, 1.); } diff --git a/src/block_placement.rs b/src/block_placement.rs new file mode 100644 index 0000000..89cdf99 --- /dev/null +++ b/src/block_placement.rs @@ -0,0 +1,37 @@ +use glam::Vec3; +use shipyard::{UniqueViewMut, UniqueView, View, IntoIter}; +use crate::{ + player::MainPlayer, + world::{raycast::LookingAtBlock, ChunkStorage, block::Block}, + input::{Inputs, PrevInputs} +}; + +pub fn block_placement_system( + main_player: View, + raycast: View, + input: UniqueView, + prev_input: UniqueView, + mut world: UniqueViewMut +) { + let action_place = input.action_b && !prev_input.0.action_b; + let action_break = input.action_a && !prev_input.0.action_a; + if action_place ^ action_break { + //get raycast info + let Some(ray) = (&main_player, &raycast).iter().next().unwrap().1/**/.0 else { return }; + //update block + let place_position = if action_place { + let position = (ray.position - ray.direction * 0.5).floor().as_ivec3(); + let Some(block) = world.get_block_mut(position) else { return }; + *block = Block::Dirt; + position + } else { + let Some(block) = world.get_block_mut(ray.block_position) else { return }; + *block = Block::Air; + ray.block_position + }; + //mark chunk as dirty + let (chunk_pos, _) = ChunkStorage::to_chunk_coords(place_position); + let chunk = world.chunks.get_mut(&chunk_pos).unwrap(); + chunk.dirty = true; + } +} diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..3da67c3 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,43 @@ +use glam::{Mat4, Vec3}; +use shipyard::{Component, Workload, IntoWorkload}; +use std::f32::consts::PI; + +mod matrices; +mod frustum; + +use matrices::update_matrices; +use frustum::{Frustum, update_frustum}; + +#[derive(Component)] +pub struct Camera { + pub view_matrix: Mat4, + pub perspective_matrix: Mat4, + pub frustum: Frustum, + pub up: Vec3, + pub fov: f32, + pub z_near: f32, + pub z_far: f32, +} +impl Camera { + pub fn new(fov: f32, z_near: f32, z_far: f32, up: Vec3) -> Self { + Self { + fov, z_near, z_far, up, + //TODO maybe separate this? + perspective_matrix: Mat4::default(), + view_matrix: Mat4::default(), + frustum: Frustum::default(), + } + } +} +impl Default for Camera { + fn default() -> Self { + Self::new(PI / 3., 0.1, 1024., Vec3::Y) + } +} + +pub fn compute_cameras() -> Workload { + ( + update_matrices, + update_frustum, + ).into_workload() +} diff --git a/src/camera/frustum.rs b/src/camera/frustum.rs new file mode 100644 index 0000000..9042dd4 --- /dev/null +++ b/src/camera/frustum.rs @@ -0,0 +1,130 @@ +// basically ported from c++ +// - used as a reference: +// [ https://github.com/Beastwick18/gltest/blob/main/src/renderer/Frustum.cpp ] +// - original code: +// [ https://gist.github.com/podgorskiy/e698d18879588ada9014768e3e82a644 ] +// - which uses cube vs frustum intersection code from: +// [ http://iquilezles.org/www/articles/frustumcorrect/frustumcorrect.htm ] +// three layers of stolen code, yay! + +use glam::{Vec3A, Vec4, Mat3A, vec3a, Vec3, vec4}; +use shipyard::{ViewMut, IntoIter, View}; +use crate::transform::Transform; +use super::Camera; + +#[repr(usize)] +enum FrustumPlane { + Left, + Right, + Bottom, + Top, + Near, + Far, +} + +const PLANE_COUNT: usize = 6; +const PLANE_COMBINATIONS: usize = PLANE_COUNT * (PLANE_COUNT - 1) / 2; +const POINT_COUNT: usize = 8; + +#[derive(Default)] +pub struct Frustum { + planes: [Vec4; PLANE_COUNT], + points: [Vec3A; POINT_COUNT] +} +impl Frustum { + pub fn compute(camera: &Camera) -> Self { + //compute transposed view-projection matrix + let mat = (camera.perspective_matrix * camera.view_matrix).transpose(); + + // compute planes + let mut planes = [Vec4::default(); PLANE_COUNT]; + planes[FrustumPlane::Left as usize] = mat.w_axis + mat.x_axis; + planes[FrustumPlane::Right as usize] = mat.w_axis - mat.x_axis; + planes[FrustumPlane::Bottom as usize] = mat.w_axis + mat.y_axis; + planes[FrustumPlane::Top as usize] = mat.w_axis - mat.y_axis; + planes[FrustumPlane::Near as usize] = mat.w_axis + mat.z_axis; + planes[FrustumPlane::Far as usize] = mat.w_axis - mat.z_axis; + + //compute crosses + let crosses = [ + Vec3A::from(planes[FrustumPlane::Left as usize]).cross(planes[FrustumPlane::Right as usize].into()), + Vec3A::from(planes[FrustumPlane::Left as usize]).cross(planes[FrustumPlane::Bottom as usize].into()), + Vec3A::from(planes[FrustumPlane::Left as usize]).cross(planes[FrustumPlane::Top as usize].into()), + Vec3A::from(planes[FrustumPlane::Left as usize]).cross(planes[FrustumPlane::Near as usize].into()), + Vec3A::from(planes[FrustumPlane::Left as usize]).cross(planes[FrustumPlane::Far as usize].into()), + Vec3A::from(planes[FrustumPlane::Right as usize]).cross(planes[FrustumPlane::Bottom as usize].into()), + Vec3A::from(planes[FrustumPlane::Right as usize]).cross(planes[FrustumPlane::Top as usize].into()), + Vec3A::from(planes[FrustumPlane::Right as usize]).cross(planes[FrustumPlane::Near as usize].into()), + Vec3A::from(planes[FrustumPlane::Right as usize]).cross(planes[FrustumPlane::Far as usize].into()), + Vec3A::from(planes[FrustumPlane::Bottom as usize]).cross(planes[FrustumPlane::Top as usize].into()), + Vec3A::from(planes[FrustumPlane::Bottom as usize]).cross(planes[FrustumPlane::Near as usize].into()), + Vec3A::from(planes[FrustumPlane::Bottom as usize]).cross(planes[FrustumPlane::Far as usize].into()), + Vec3A::from(planes[FrustumPlane::Top as usize]).cross(planes[FrustumPlane::Near as usize].into()), + Vec3A::from(planes[FrustumPlane::Top as usize]).cross(planes[FrustumPlane::Far as usize].into()), + Vec3A::from(planes[FrustumPlane::Near as usize]).cross(planes[FrustumPlane::Far as usize].into()), + ]; + + //compute points + let points = [ + intersection::<{FrustumPlane::Left as usize}, {FrustumPlane::Bottom as usize}, {FrustumPlane::Near as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Left as usize}, {FrustumPlane::Top as usize}, {FrustumPlane::Near as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Right as usize}, {FrustumPlane::Bottom as usize}, {FrustumPlane::Near as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Right as usize}, {FrustumPlane::Top as usize}, {FrustumPlane::Near as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Left as usize}, {FrustumPlane::Bottom as usize}, {FrustumPlane::Far as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Left as usize}, {FrustumPlane::Top as usize}, {FrustumPlane::Far as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Right as usize}, {FrustumPlane::Bottom as usize}, {FrustumPlane::Far as usize}>(&planes, &crosses), + intersection::<{FrustumPlane::Right as usize}, {FrustumPlane::Top as usize}, {FrustumPlane::Far as usize}>(&planes, &crosses), + ]; + + Self { planes, points } + } + + pub fn is_box_visible(&self, minp: Vec3, maxp: Vec3) -> bool { + // check box outside/inside of frustum + for plane in self.planes { + if (plane.dot(vec4(minp.x, minp.y, minp.z, 1.)) < 0.) && + (plane.dot(vec4(maxp.x, minp.y, minp.z, 1.)) < 0.) && + (plane.dot(vec4(minp.x, maxp.y, minp.z, 1.)) < 0.) && + (plane.dot(vec4(maxp.x, maxp.y, minp.z, 1.)) < 0.) && + (plane.dot(vec4(minp.x, minp.y, maxp.z, 1.)) < 0.) && + (plane.dot(vec4(maxp.x, minp.y, maxp.z, 1.)) < 0.) && + (plane.dot(vec4(minp.x, maxp.y, maxp.z, 1.)) < 0.) && + (plane.dot(vec4(maxp.x, maxp.y, maxp.z, 1.)) < 0.) + { + return false + } + } + + // check frustum outside/inside box + if self.points.iter().all(|point| point.x > maxp.x) { return false } + if self.points.iter().all(|point| point.x < minp.x) { return false } + if self.points.iter().all(|point| point.y > maxp.y) { return false } + if self.points.iter().all(|point| point.y < minp.y) { return false } + if self.points.iter().all(|point| point.z > maxp.z) { return false } + if self.points.iter().all(|point| point.z < minp.z) { return false } + + true + } +} + +const fn ij2k() -> usize { + I * (9 - I) / 2 + J - 1 +} +fn intersection(planes: &[Vec4; PLANE_COUNT], crosses: &[Vec3A; PLANE_COMBINATIONS]) -> Vec3A { + let d = Vec3A::from(planes[A]).dot(crosses[ij2k::()]); + let res = Mat3A::from_cols( + crosses[ij2k::()], + -crosses[ij2k::()], + crosses[ij2k::()], + ) * vec3a(planes[A].w, planes[B].w, planes[C].w); + res * (-1. / d) +} + +pub fn update_frustum( + mut cameras: ViewMut, + transforms: View +) { + for (camera, _) in (&mut cameras, transforms.inserted_or_modified()).iter() { + camera.frustum = Frustum::compute(camera); + } +} diff --git a/src/camera/matrices.rs b/src/camera/matrices.rs new file mode 100644 index 0000000..b2086f6 --- /dev/null +++ b/src/camera/matrices.rs @@ -0,0 +1,42 @@ +use glam::{Vec3, Mat4}; +use shipyard::{ViewMut, View, IntoIter, Workload, IntoWorkload}; +use crate::{transform::Transform, events::WindowResizedEvent}; +use super::Camera; + +//maybe parallelize these two? + +fn update_view_matrix( + mut vm_camera: ViewMut, + v_transform: View +) { + for (camera, transform) in (&mut vm_camera, v_transform.inserted_or_modified()).iter() { + let (_, rotation, translation) = transform.0.to_scale_rotation_translation(); + let direction = rotation * Vec3::NEG_Z; + camera.view_matrix = Mat4::look_to_rh(translation, direction, camera.up); + } +} + +fn update_perspective_matrix( + mut vm_camera: ViewMut, + resize: View, +) { + //TODO update on launch + let Some(&size) = resize.iter().next() else { + return + }; + for camera in (&mut vm_camera).iter() { + camera.perspective_matrix = Mat4::perspective_rh_gl( + camera.fov, + size.0.x as f32 / size.0.y as f32, + camera.z_near, + camera.z_far, + ) + } +} + +pub fn update_matrices() -> Workload { + ( + update_view_matrix, + update_perspective_matrix, + ).into_workload() +} diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..3a37298 --- /dev/null +++ b/src/events.rs @@ -0,0 +1,55 @@ +use glam::UVec2; +use shipyard::{World, Component, AllStoragesViewMut, SparseSet}; +use glium::glutin::event::{Event, DeviceEvent, DeviceId, WindowEvent}; + +#[derive(Component, Clone, Copy, Debug, Default)] +pub struct EventComponent; + +#[derive(Component, Clone, Copy, Debug, Default)] +pub struct OnBeforeExitEvent; + +#[derive(Component, Clone, Debug)] +pub struct InputDeviceEvent{ + pub device_id: DeviceId, + pub event: DeviceEvent +} + +#[derive(Component, Clone, Copy, Debug, Default)] +pub struct WindowResizedEvent(pub UVec2); + +pub fn process_glutin_events(world: &mut World, event: &Event<'_, ()>) { + #[allow(clippy::collapsible_match, clippy::single_match)] + match event { + Event::WindowEvent { window_id: _, event } => match event { + WindowEvent::Resized(size) => { + world.add_entity(( + EventComponent, + WindowResizedEvent(UVec2::new(size.width as _, size.height as _)) + )); + }, + _ => () + }, + Event::DeviceEvent { device_id, event } => { + world.add_entity(( + EventComponent, + InputDeviceEvent { + device_id: *device_id, + event: event.clone() + } + )); + }, + Event::LoopDestroyed => { + world.add_entity(( + EventComponent, + OnBeforeExitEvent + )); + }, + _ => (), + } +} + +pub fn clear_events( + mut all_storages: AllStoragesViewMut, +) { + all_storages.delete_any::>(); +} diff --git a/src/fly_controller.rs b/src/fly_controller.rs new file mode 100644 index 0000000..a8972ea --- /dev/null +++ b/src/fly_controller.rs @@ -0,0 +1,52 @@ +use glam::{Vec3, Mat4, Quat, EulerRot, Vec2}; +use shipyard::{Component, View, ViewMut, IntoIter, UniqueView, Workload, IntoWorkload}; +use std::f32::consts::PI; +use crate::{transform::Transform, input::Inputs, settings::GameSettings, DeltaTime}; + +#[derive(Component)] +pub struct FlyController; + +pub fn update_controllers() -> Workload { + ( + update_look, + update_movement + ).into_workload() +} + +const MAX_PITCH: f32 = PI/2. - 0.001; + +fn update_look( + controllers: View, + mut transforms: ViewMut, + inputs: UniqueView, + settings: UniqueView, + dt: UniqueView, +) { + let look = inputs.look * settings.mouse_sensitivity * dt.0.as_secs_f32(); + if look == Vec2::ZERO { return } + for (_, mut transform) in (&controllers, &mut transforms).iter() { + let (scale, mut rotation, translation) = transform.0.to_scale_rotation_translation(); + let (mut yaw, mut pitch, _roll) = rotation.to_euler(EulerRot::YXZ); + yaw -= look.x; + pitch -= look.y; + pitch = pitch.clamp(-MAX_PITCH, MAX_PITCH); + rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, 0.).normalize(); + transform.0 = Mat4::from_scale_rotation_translation(scale, rotation, translation); + } +} + +fn update_movement( + controllers: View, + mut transforms: ViewMut, + inputs: UniqueView, + dt: UniqueView, +) { + let movement = inputs.movement * 30. * dt.0.as_secs_f32(); + if movement == Vec2::ZERO { return } + for (_, mut transform) in (&controllers, &mut transforms).iter() { + let (scale, rotation, mut translation) = transform.0.to_scale_rotation_translation(); + translation += (rotation * Vec3::NEG_Z) * movement.y; + translation += (rotation * Vec3::X) * movement.x; + transform.0 = Mat4::from_scale_rotation_translation(scale, rotation, translation); + } +} diff --git a/src/game.rs b/src/game.rs deleted file mode 100644 index 3c9d308..0000000 --- a/src/game.rs +++ /dev/null @@ -1,162 +0,0 @@ -use glam::Vec2; -use glium::Surface; -use glium::glutin::{ - event::{Event, WindowEvent, DeviceEvent}, - event_loop::{EventLoop, ControlFlow}, -}; -use std::time::Instant; - -mod assets; -mod display; -mod shaders; -mod camera; -mod controller; -mod world; -mod blocks; -mod items; -mod options; -mod physics; -mod player; - -use assets::Assets; -use display::init_display; -use shaders::Programs; -use camera::Camera; -use controller::Controls; -use world::World; -use options::GameOptions; - -struct State { - pub camera: Camera, - pub first_draw: bool, - pub controls: Controls, - pub world: World -} -impl State { - pub fn init() -> Self { - Self { - first_draw: true, - camera: Camera::default(), - controls: Controls::default(), - world: World::new(), - } - } -} - -pub fn run() { - log::info!("starting up"); - let event_loop = EventLoop::new(); - log::info!("initializing display"); - let display = init_display(&event_loop); - log::info!("compiling shaders"); - let programs = Programs::compile_all(&display); - log::info!("loading assets"); - let assets = Assets::load_all_sync(&display); - log::info!("init game options"); - let options = GameOptions::default(); - log::info!("init game state"); - let mut state = State::init(); - state.camera.position = [0., 260., -1.]; - log::info!("game loaded"); - - //======================= - // let vertex1 = ChunkVertex { position: [-0.5, -0.5, 0.], uv: [0., 0.], normal: [0., 1., 0.] }; - // let vertex2 = ChunkVertex { position: [ 0.0, 0.5, 0.], uv: [0., 1.], normal: [0., 1., 0.] }; - // let vertex3 = ChunkVertex { position: [ 0.5, -0.5, 0.], uv: [1., 1.], normal: [0., 1., 0.] }; - // let shape = vec![vertex1, vertex2, vertex3]; - // let vertex_buffer = glium::VertexBuffer::new(&display, &shape).unwrap(); - //======================= - - let mut last_render = Instant::now(); - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Poll; - match event { - // Mouse motion - Event::DeviceEvent { - event: DeviceEvent::MouseMotion{ delta, }, .. - } => { - state.controls.process_mouse_input(delta.0, delta.1); - return - } - // Keyboard input - Event::DeviceEvent { event: DeviceEvent::Key(input), .. } => { - if let Some(key) = input.virtual_keycode { - state.controls.process_keyboard_input(key, input.state); - } - return - } - // Window events - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CloseRequested => { - log::info!("exit requested"); - *control_flow = ControlFlow::Exit; - return - }, - WindowEvent::Resized(size) => { - state.camera.update_perspective_matrix((size.width, size.height)); - }, - _ => return - } - }, - Event::MainEventsCleared => (), - _ => return - } - - //Calculate delta time - let now = Instant::now(); - let dt = (now - last_render).as_secs_f32(); - last_render = now; - - //Update controls - state.controls.calculate(dt).apply_to_camera(&mut state.camera); - - //Load new chunks - - state.world.update_loaded_chunks( - Vec2::new(state.camera.position[0], state.camera.position[2]), - &options, - &display - ); - - //Start drawing - let mut target = display.draw(); - target.clear_color_and_depth((0.5, 0.5, 1., 1.), 1.); - - //Compute camera - if state.first_draw { - let target_dimensions = target.get_dimensions(); - state.camera.update_perspective_matrix(target_dimensions); - } - let perspective = state.camera.perspective_matrix; - let view = state.camera.view_matrix(); - - //Draw chunks - state.world.render(&mut target, &programs, &assets, perspective, view, &options); - - //Draw example triangle - // target.draw( - // &vertex_buffer, - // glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList), - // &programs.chunk, - // &uniform! { - // model: [ - // [1., 0., 0., 0.], - // [0., 1., 0., 0.], - // [0., 0., 1., 0.], - // [0., 0., 0., 1.0_f32] - // ], - // view: view, - // perspective: perspective, - // tex: Sampler(&assets.textures.block_atlas, sampler_nearest) - // }, - // &Default::default() - // ).unwrap(); - - //Finish drawing - target.finish().unwrap(); - - state.first_draw = false; - }); -} diff --git a/src/game/assets.rs b/src/game/assets.rs deleted file mode 100644 index 27687a9..0000000 --- a/src/game/assets.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod textures; - -use textures::Textures; - -pub struct Assets { - pub textures: Textures -} -impl Assets { - /// Load all assets synchronously - pub fn load_all_sync(display: &glium::Display) -> Self { - Self { - textures: Textures::load_sync(display) - } - } -} diff --git a/src/game/assets/textures.rs b/src/game/assets/textures.rs deleted file mode 100644 index c7cf857..0000000 --- a/src/game/assets/textures.rs +++ /dev/null @@ -1,103 +0,0 @@ -use std::{fs, io, path::PathBuf, sync::atomic::AtomicU16}; -use rayon::prelude::*; -use glium::texture::{RawImage2d, SrgbTexture2d, SrgbTexture2dArray}; - -//This code is terrible and has a alot of duplication - -fn load_png(file_path: &str, display: &glium::Display) -> SrgbTexture2d { - log::info!("loading texture {}", file_path); - - //Load file - let data = fs::read(file_path) - .unwrap_or_else(|_| panic!("Failed to load texture: {}", file_path)); - - //decode image data - let image_data = image::load( - io::Cursor::new(&data), - image::ImageFormat::Png - ).unwrap().to_rgba8(); - - //Create raw glium image - let image_dimensions = image_data.dimensions(); - let raw_image = RawImage2d::from_raw_rgba_reversed( - &image_data.into_raw(), - image_dimensions - ); - - //Create texture - SrgbTexture2d::new(display, raw_image).unwrap() -} - -fn load_png_array(file_paths: &[PathBuf], display: &glium::Display) -> SrgbTexture2dArray { - let counter = AtomicU16::new(0); - let raw_images: Vec> = file_paths.par_iter().enumerate().map(|(_, file_path)| { - - let fname: &str = file_path.file_name().unwrap_or_default().to_str().unwrap(); - - //Load file - let data = fs::read(file_path).expect(&format!("Failed to load texture {}", fname)); - - //decode image data - let image_data = image::load( - io::Cursor::new(&data), - image::ImageFormat::Png - ).unwrap().to_rgba8(); - - //Create raw glium image - let image_dimensions = image_data.dimensions(); - let raw_image = RawImage2d::from_raw_rgba_reversed( - &image_data.into_raw(), - image_dimensions - ); - - let counter = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; - log::info!("loaded texture {}/{}: {}", counter, file_paths.len(), fname); - - raw_image - }).collect(); - SrgbTexture2dArray::new(display, raw_images).unwrap() -} - -pub struct Textures { - pub blocks: SrgbTexture2dArray -} -impl Textures { - /// Load textures synchronously, one by one and upload them to the GPU - pub fn load_sync(display: &glium::Display) -> Self { - Self { - blocks: load_png_array(&[ - "./assets/blocks/stone.png".into(), - "./assets/blocks/dirt.png".into(), - "./assets/blocks/grass_top.png".into(), - "./assets/blocks/grass_side.png".into(), - "./assets/blocks/sand.png".into(), - "./assets/blocks/bedrock.png".into(), - "./assets/blocks/wood.png".into(), - "./assets/blocks/wood_top.png".into(), - "./assets/blocks/leaf.png".into(), - "./assets/blocks/torch.png".into(), - "./assets/blocks/tall_grass.png".into(), - "./assets/blocks/snow.png".into(), - "./assets/blocks/grass_side_snow.png".into(), - ], display) - } - } -} - -#[derive(Clone, Copy, Debug)] -#[repr(u8)] -pub enum BlockTexture { - Stone = 0, - Dirt = 1, - GrassTop = 2, - GrassSide = 3, - Sand = 4, - Bedrock = 5, - Wood = 6, - WoodTop = 7, - Leaf = 8, - Torch = 9, - TallGrass = 10, - Snow = 11, - GrassSideSnow = 12, -} diff --git a/src/game/blocks.rs b/src/game/blocks.rs deleted file mode 100644 index 9db47b6..0000000 --- a/src/game/blocks.rs +++ /dev/null @@ -1,141 +0,0 @@ -use strum::{EnumIter, IntoEnumIterator}; -use crate::game::{ - items::Item, - assets::textures::BlockTexture, -}; - - -#[derive(Clone, Copy, Debug)] -pub enum CollisionType { - Solid, - Liquid, - Ladder, -} - -#[derive(Clone, Copy, Debug)] -pub enum RenderType { - OpaqueBlock, - TranslucentBlock, - TranslucentLiquid, - CrossShape -} - -#[derive(Clone, Copy, Debug)] -pub struct BlockTextures { - pub top: BlockTexture, - pub bottom: BlockTexture, - pub left: BlockTexture, - pub right: BlockTexture, - pub back: BlockTexture, - pub front: BlockTexture, -} -impl BlockTextures { - pub const fn all(tex: BlockTexture) -> Self { - Self { - top: tex, - bottom: tex, - left: tex, - right: tex, - back: tex, - front: tex, - } - } - pub const fn top_sides_bottom(top: BlockTexture, sides: BlockTexture, bottom: BlockTexture) -> Self { - Self { - top, - bottom, - left: sides, - right: sides, - back: sides, - front: sides, - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct BlockDescriptor { - pub name: &'static str, - pub id: &'static str, - pub collision: Option, - pub raycast_collision: bool, - pub render: Option<(RenderType, BlockTextures)>, - pub item: Option, -} -impl BlockDescriptor { - //Not using the Default trait because this function has to be const! - pub const fn default() -> Self { - Self { - name: "default", - id: "default", - collision: Some(CollisionType::Solid), - raycast_collision: true, - render: Some((RenderType::OpaqueBlock, BlockTextures::all(BlockTexture::Stone))), - item: None - } - } -} - -#[derive(Clone, Copy, Debug, EnumIter)] -pub enum Block { - Air, - Stone, - Dirt, - Grass, - Sand, -} -impl Block { - //TODO make this O(1) with compile-time computed maps - pub fn get_by_id(id: &str) -> Option { - for block in Self::iter() { - if block.descriptor().id == id { - return Some(block) - } - } - None - } - - pub const fn descriptor(self) -> BlockDescriptor { - match self { - Self::Air => BlockDescriptor { - name: "Air", - id: "air", - collision: None, - raycast_collision: false, - render: None, - item: None, - }, - Self::Stone => BlockDescriptor { - name: "Stone", - id: "stone", - collision: Some(CollisionType::Solid), - raycast_collision: true, - render: Some((RenderType::OpaqueBlock, BlockTextures::all(BlockTexture::Stone))), - item: Some(Item::StoneBlock) - }, - Self::Dirt => BlockDescriptor { - name: "Dirt", - id: "dirt", - collision: Some(CollisionType::Solid), - raycast_collision: true, - render: Some((RenderType::OpaqueBlock, BlockTextures::all(BlockTexture::Dirt))), - item: Some(Item::DirtBlock) - }, - Self::Grass => BlockDescriptor { - name: "Grass", - id: "grass", - collision: Some(CollisionType::Solid), - raycast_collision: true, - render: Some((RenderType::OpaqueBlock, BlockTextures::top_sides_bottom(BlockTexture::GrassTop, BlockTexture::GrassSide, BlockTexture::Dirt))), - item: Some(Item::DirtBlock) - }, - Self::Sand => BlockDescriptor { - name: "Sand", - id: "sand", - collision: Some(CollisionType::Solid), - raycast_collision: true, - render: Some((RenderType::OpaqueBlock, BlockTextures::all(BlockTexture::Sand))), //this is not a sand tex - item: Some(Item::StoneBlock) - } - } - } -} diff --git a/src/game/camera.rs b/src/game/camera.rs deleted file mode 100644 index 5dc2ce6..0000000 --- a/src/game/camera.rs +++ /dev/null @@ -1,157 +0,0 @@ -// Perspective/View matrix code from: -// https://glium.github.io/glium/book/tuto-10-perspective.html -// https://glium.github.io/glium/book/tuto-12-camera.html -// I don't understand anything but it works - -use std::f32::consts::PI; - -pub fn calculate_forward_direction(yaw: f32, pitch: f32) -> [f32; 3] { - [ - yaw.cos() * pitch.cos(), - pitch.sin(), - yaw.sin() * pitch.cos(), - ] -} - -fn normalize_plane(mut plane: [f32; 4]) -> [f32; 4] { - let mag = (plane[0] * plane[0] + plane[1] * plane[1] + plane[2] * plane[2]).sqrt(); - plane[0] = plane[0] / mag; - plane[1] = plane[1] / mag; - plane[2] = plane[2] / mag; - plane[3] = plane[3] / mag; - plane -} - -pub struct Camera { - pub yaw: f32, - pub pitch: f32, - pub position: [f32; 3], - pub direction: [f32; 3], - pub up: [f32; 3], - pub fov: f32, - pub znear: f32, - pub zfar: f32, - pub perspective_matrix: [[f32; 4]; 4], -} -impl Camera { - /// Update camera direction based on yaw/pitch - pub fn update_direction(&mut self) { - self.direction = calculate_forward_direction(self.yaw, self.pitch); - } - pub fn forward(&mut self, amount: f32) { - self.position[0] += self.direction[0] * amount; - self.position[1] += self.direction[1] * amount; - self.position[2] += self.direction[2] * amount; - } - - pub fn view_matrix(&self) -> [[f32; 4]; 4] { - let position = self.position; - let direction = self.direction; - let up = self.up; - - let f = { - let f = direction; - let len = f[0] * f[0] + f[1] * f[1] + f[2] * f[2]; - let len = len.sqrt(); - [f[0] / len, f[1] / len, f[2] / len] - }; - let s = [up[1] * f[2] - up[2] * f[1], - up[2] * f[0] - up[0] * f[2], - up[0] * f[1] - up[1] * f[0]]; - let s_norm = { - let len = s[0] * s[0] + s[1] * s[1] + s[2] * s[2]; - let len = len.sqrt(); - [s[0] / len, s[1] / len, s[2] / len] - }; - let u = [f[1] * s_norm[2] - f[2] * s_norm[1], - f[2] * s_norm[0] - f[0] * s_norm[2], - f[0] * s_norm[1] - f[1] * s_norm[0]]; - let p = [-position[0] * s_norm[0] - position[1] * s_norm[1] - position[2] * s_norm[2], - -position[0] * u[0] - position[1] * u[1] - position[2] * u[2], - -position[0] * f[0] - position[1] * f[1] - position[2] * f[2]]; - [ - [s_norm[0], u[0], f[0], 0.0], - [s_norm[1], u[1], f[1], 0.0], - [s_norm[2], u[2], f[2], 0.0], - [p[0], p[1], p[2], 1.0], - ] - } - - pub fn update_perspective_matrix(&mut self, target_dimensions: (u32, u32)) { - let znear = self.znear; - let zfar = self.zfar; - let fov = self.fov; - let (width, height) = target_dimensions; - let aspect_ratio = height as f32 / width as f32; - let f = 1.0 / (fov / 2.0).tan(); - self.perspective_matrix = [ - [f*aspect_ratio, 0.0, 0.0, 0.0], - [0.0, f, 0.0, 0.0], - [0.0, 0.0, (zfar+znear)/(zfar-znear), 1.0], - [0.0, 0.0, -(2.0*zfar*znear)/(zfar-znear), 0.0], - ]; - } - - // https://www.flipcode.com/archives/Frustum_Culling.shtml - // https://web.archive.org/web/20070226173353/https://www2.ravensoft.com/users/ggribb/plane%20extraction.pdf - pub fn frustum_planes(&self, normalized: bool) -> [[f32; 4]; 6] { - let mut p_planes = [[0.0_f32; 4]; 6]; - let matrix = self.perspective_matrix; - - // Left clipping plane - p_planes[0][0] = matrix[3][0] + matrix[0][0]; - p_planes[0][1] = matrix[3][1] + matrix[0][1]; - p_planes[0][2] = matrix[3][2] + matrix[0][2]; - p_planes[0][3] = matrix[3][3] + matrix[0][3]; - // Right clipping plane - p_planes[1][0] = matrix[3][0] - matrix[0][0]; - p_planes[1][1] = matrix[3][1] - matrix[0][1]; - p_planes[1][2] = matrix[3][2] - matrix[0][2]; - p_planes[1][3] = matrix[3][3] - matrix[0][3]; - // Top clipping plane - p_planes[2][0] = matrix[3][0] - matrix[1][0]; - p_planes[2][1] = matrix[3][1] - matrix[1][1]; - p_planes[2][2] = matrix[3][2] - matrix[1][2]; - p_planes[2][3] = matrix[3][3] - matrix[1][3]; - // Bottom clipping plane - p_planes[3][0] = matrix[3][0] + matrix[1][0]; - p_planes[3][1] = matrix[3][1] + matrix[1][1]; - p_planes[3][2] = matrix[3][2] + matrix[1][2]; - p_planes[3][3] = matrix[3][3] + matrix[1][3]; - // Near clipping plane - p_planes[4][0] = matrix[3][0] + matrix[3][0]; - p_planes[4][1] = matrix[3][1] + matrix[3][1]; - p_planes[4][2] = matrix[3][2] + matrix[3][2]; - p_planes[4][3] = matrix[3][3] + matrix[3][3]; - // Far clipping plane - p_planes[5][0] = matrix[3][0] - matrix[3][0]; - p_planes[5][1] = matrix[3][1] - matrix[3][1]; - p_planes[5][2] = matrix[3][2] - matrix[3][2]; - p_planes[5][3] = matrix[3][3] - matrix[3][3]; - - //Normalize planes - if normalized { - for plane in &mut p_planes { - *plane = normalize_plane(*plane); - } - } - - p_planes - } - -} -impl Default for Camera { - fn default() -> Self { - Self { - position: [0., 0., 0.], - direction: [0., 0., 0.], - up: [0., 1., 0.], - fov: PI / 3., - zfar: 1024., - znear: 0.1, - yaw: 0., - pitch: 0., - perspective_matrix: [[0.; 4]; 4] - } - } -} diff --git a/src/game/controller.rs b/src/game/controller.rs deleted file mode 100644 index 58b5b29..0000000 --- a/src/game/controller.rs +++ /dev/null @@ -1,116 +0,0 @@ -use glium::glutin::event::{VirtualKeyCode, ElementState}; -use std::f32::consts::PI; -use crate::game::camera::Camera; - -#[derive(Default, Clone, Copy)] -pub struct InputAmounts { - move_x: (f32, f32), - move_y: (f32, f32), - move_z: (f32, f32), - look_h: f32, - look_v: f32, -} - -pub struct Actions { - pub movement: [f32; 3], - pub rotation: [f32; 2], -} -impl Actions { - pub fn apply_to_camera(&self, camera: &mut Camera) { - //Apply rotation - camera.yaw -= self.rotation[0]; - camera.pitch -= self.rotation[1]; - camera.pitch = camera.pitch.clamp(-PI/2. + f32::EPSILON, PI/2. - f32::EPSILON); - camera.update_direction(); - //Apply movement - let (yaw_sin, yaw_cos) = camera.yaw.sin_cos(); - //forward movement - camera.position[0] += yaw_cos * self.movement[2]; - camera.position[2] += yaw_sin * self.movement[2]; - //sideways movement - camera.position[0] -= -yaw_sin * self.movement[0]; - camera.position[2] -= yaw_cos * self.movement[0]; - //up/down movement - camera.position[1] += self.movement[1]; - } -} - -pub struct Controls { - inputs: InputAmounts, - pub speed: f32, - pub sensitivity: f32, -} -impl Controls { - //TODO locking controls - pub fn lock(&mut self) { - todo!() - } - pub fn unlock(&mut self) { - todo!() - } - pub fn process_mouse_input(&mut self, dx: f64, dy: f64) { - self.inputs.look_h += dx as f32; - self.inputs.look_v += dy as f32; - } - pub fn process_keyboard_input(&mut self, key: VirtualKeyCode, state: ElementState) { - let value = match state { - ElementState::Pressed => 1., - ElementState::Released => 0., - }; - match key { - VirtualKeyCode::W | VirtualKeyCode::Up => { - self.inputs.move_z.0 = value; - } - VirtualKeyCode::S | VirtualKeyCode::Down => { - self.inputs.move_z.1 = -value; - } - VirtualKeyCode::A | VirtualKeyCode::Left => { - self.inputs.move_x.0 = -value; - } - VirtualKeyCode::D | VirtualKeyCode::Right => { - self.inputs.move_x.1 = value; - } - VirtualKeyCode::Space => { - self.inputs.move_y.0 = value; - } - VirtualKeyCode::LShift => { - self.inputs.move_y.1 = -value; - } - _ => () - } - } - pub fn calculate(&mut self, dt: f32) -> Actions { - let movement = { - let move_x = self.inputs.move_x.0 + self.inputs.move_x.1; - let move_y = self.inputs.move_y.0 + self.inputs.move_y.1; - let move_z = self.inputs.move_z.0 + self.inputs.move_z.1; - let magnitude = (move_x.powi(2) + move_y.powi(2) + move_z.powi(2)).sqrt(); - if magnitude == 0. { - [0., 0., 0.] - } else { - [ - dt * self.speed * (move_x / magnitude), - dt * self.speed * (move_y / magnitude), - dt * self.speed * (move_z / magnitude) - ] - } - }; - let rotation = [ //Do mouse inputs need to be multiplied by dt? - self.inputs.look_h * self.sensitivity * 0.01, //* dt - self.inputs.look_v * self.sensitivity * 0.01 //* dt - ]; - //Only mouse related actions need to be reset - self.inputs.look_h = 0.; - self.inputs.look_v = 0.; - Actions { movement, rotation } - } -} -impl Default for Controls { - fn default() -> Self { - Self { - inputs: Default::default(), - speed: 40., - sensitivity: 1.24, - } - } -} diff --git a/src/game/display.rs b/src/game/display.rs deleted file mode 100644 index 97d7e02..0000000 --- a/src/game/display.rs +++ /dev/null @@ -1,16 +0,0 @@ -use glium::Display; -use glium::glutin::{ - ContextBuilder, - GlProfile, - window::WindowBuilder, - event_loop::EventLoop -}; - -pub fn init_display(event_loop: &EventLoop<()>) -> Display { - let wb = WindowBuilder::new() - .with_maximized(true); - let cb = ContextBuilder::new() - .with_depth_buffer(24) - .with_gl_profile(GlProfile::Core); - Display::new(wb, cb, event_loop).unwrap() -} diff --git a/src/game/items.rs b/src/game/items.rs deleted file mode 100644 index 0028d1c..0000000 --- a/src/game/items.rs +++ /dev/null @@ -1,9 +0,0 @@ -//TODO items - -#[derive(Clone, Copy, Debug)] -pub enum Item { - StoneBlock, - DirtBlock, - GrassBlock, - SandBlock, -} diff --git a/src/game/options.rs b/src/game/options.rs deleted file mode 100644 index 7a19998..0000000 --- a/src/game/options.rs +++ /dev/null @@ -1,13 +0,0 @@ -#[derive(Clone, Debug)] -pub struct GameOptions { - pub render_distance: u8, - pub debug_wireframe_mode: bool, -} -impl Default for GameOptions { - fn default() -> Self { - Self { - render_distance: if cfg!(debug_assertions) { 8 } else { 16 }, - debug_wireframe_mode: false, - } - } -} diff --git a/src/game/physics.rs b/src/game/physics.rs deleted file mode 100644 index a0e0d2c..0000000 --- a/src/game/physics.rs +++ /dev/null @@ -1,46 +0,0 @@ -use glam::{Vec3A, vec3a}; -use crate::game::World; - -const GRAVITY: Vec3A = vec3a(0., -1., 0.); - -pub struct BasicPhysicsActor { - pub height: f32, - pub gravity: Vec3A, - pub position: Vec3A, - pub velocity: Vec3A, -} -impl BasicPhysicsActor { - pub fn new(height: f32) -> Self { - Self { - height, - gravity: GRAVITY, - position: vec3a(0., 0., 0.), - velocity: vec3a(0., 0., 0.), - } - } - pub fn update(&mut self, world: &World, dt: f32) { - self.velocity += GRAVITY; - self.position += self.velocity; - loop { - let block_pos = self.position.floor().as_ivec3(); - let block_pos_f = block_pos.as_vec3a(); - if let Some(block) = world.try_get(block_pos) { - match block.descriptor().collision { - Some(super::blocks::CollisionType::Solid) => { - let position_delta = self.position - block_pos_f; - let distance_to_zero = position_delta.abs(); - let distance_to_one = (vec3a(1., 1., 1.) - position_delta).abs(); - - // let mut max_distance = 0; - // let mut max_distance_normal = 0; - // distance_to_one.x - //todo compute restitution here - } - _ => break - } - } else { - break - } - } - } -} diff --git a/src/game/player.rs b/src/game/player.rs deleted file mode 100644 index 8fde307..0000000 --- a/src/game/player.rs +++ /dev/null @@ -1,7 +0,0 @@ -use crate::game::camera::Camera; -use crate::game::physics::BasicPhysicsActor; - -pub struct MainPlayer { - pub camera: Camera, - pub actor: BasicPhysicsActor, -} diff --git a/src/game/shaders.rs b/src/game/shaders.rs deleted file mode 100644 index d949234..0000000 --- a/src/game/shaders.rs +++ /dev/null @@ -1,17 +0,0 @@ -use glium::{Display, Program}; - -pub mod chunk; -pub mod colored2d; - -pub struct Programs { - pub colored_2d: Program, - pub chunk: Program, -} -impl Programs { - pub fn compile_all(display: &Display) -> Self { - Self { - colored_2d: Program::from_source(display, colored2d::VERTEX_SHADER, colored2d::FRAGMENT_SHADER, None).unwrap(), - chunk: Program::from_source(display, chunk::VERTEX_SHADER, chunk::FRAGMENT_SHADER, None).unwrap(), - } - } -} diff --git a/src/game/shaders/chunk.rs b/src/game/shaders/chunk.rs deleted file mode 100644 index 03c47e6..0000000 --- a/src/game/shaders/chunk.rs +++ /dev/null @@ -1,34 +0,0 @@ -use glium::implement_vertex; - -#[derive(Clone, Copy)] -pub struct Vertex { - pub position: [f32; 3], - pub normal: [f32; 3], - pub uv: [f32; 2], - pub tex_index: u8, -} -implement_vertex!(Vertex, position, normal, uv, tex_index); - -pub const VERTEX_SHADER: &str = include_str!("./glsl/chunk.vert"); -pub const FRAGMENT_SHADER: &str = include_str!("./glsl/chunk.frag"); - -// pub const VERTEX_SHADER: &str = r#" -// #version 150 core - -// in vec3 position; -// in vec3 normal; -// in vec2 uv; -// out vec3 v_normal; -// out vec2 v_uv; -// uniform mat4 perspective; -// uniform mat4 view; -// uniform mat4 model; - -// void main() { -// mat4 modelview = view * model; -// //v_normal = transpose(inverse(mat3(modelview))) * normal; -// v_normal = normal; -// v_uv = uv; -// gl_Position = perspective * modelview * vec4(position, 1.0); -// } -// "#; diff --git a/src/game/shaders/colored2d.rs b/src/game/shaders/colored2d.rs deleted file mode 100644 index 336dab8..0000000 --- a/src/game/shaders/colored2d.rs +++ /dev/null @@ -1,10 +0,0 @@ -use glium::implement_vertex; - -#[derive(Clone, Copy)] -pub struct Vertex { - pub position: [f32; 2] -} -implement_vertex!(Vertex, position); - -pub const VERTEX_SHADER: &str = include_str!("./glsl/colored2d.vert"); -pub const FRAGMENT_SHADER: &str = include_str!("./glsl/colored2d.frag"); diff --git a/src/game/shaders/glsl/colored2d.vert b/src/game/shaders/glsl/colored2d.vert deleted file mode 100644 index af1807e..0000000 --- a/src/game/shaders/glsl/colored2d.vert +++ /dev/null @@ -1,7 +0,0 @@ -#version 150 core - -in vec2 position; - -void main() { - gl_Position = vec4(position, 0., 1.); -} diff --git a/src/game/world.rs b/src/game/world.rs deleted file mode 100644 index de8ff50..0000000 --- a/src/game/world.rs +++ /dev/null @@ -1,201 +0,0 @@ -use glam::{Vec2, IVec2, IVec3, Vec3Swizzles}; -use glium::{ - Display, Frame, Surface, - DrawParameters, Depth, - DepthTest, PolygonMode, - uniform, - uniforms::{ - Sampler, SamplerBehavior, - MinifySamplerFilter, MagnifySamplerFilter, - } -}; -use hashbrown::HashMap; -use crate::game::{ - options::GameOptions, - shaders::Programs, - assets::Assets, - blocks::Block, -}; - -mod chunk; -mod thread; - -use chunk::{Chunk, ChunkState, CHUNK_SIZE}; -use thread::WorldThreading; - -const POSITIVE_X_NEIGHBOR: usize = 0; -const NEGATIVE_X_NEIGHBOR: usize = 1; -const POSITIVE_Z_NEIGHBOR: usize = 2; -const NEGATIVE_Z_NEIGHBOR: usize = 3; - -const MAX_TASKS: usize = 6; - -pub struct World { - pub chunks: HashMap, - pub thread: WorldThreading, -} -impl World { - pub fn chunk_neighbors(&self, position: IVec2) -> [Option<&Chunk>; 4] { - [ - self.chunks.get(&(position + IVec2::new(1, 0))), - self.chunks.get(&(position - IVec2::new(1, 0))), - self.chunks.get(&(position + IVec2::new(0, 1))), - self.chunks.get(&(position - IVec2::new(0, 1))), - ] - } - - pub fn try_get(&self, position: IVec3) -> Option { - let chunk_coord = IVec2::new(position.x, position.z) / CHUNK_SIZE as i32; - let chunk = self.chunks.get(&chunk_coord)?; - let block_data = chunk.block_data.as_ref()?; - let block_position = position - (chunk_coord * CHUNK_SIZE as i32).extend(0).xzy(); - Some( - *block_data - .get(block_position.x as usize)? - .get(block_position.y as usize)? - .get(block_position.z as usize)? - ) - } - - pub fn new() -> Self { - Self { - chunks: HashMap::new(), - thread: WorldThreading::new(), - } - } - - pub fn render( - &self, - target: &mut Frame, - programs: &Programs, - assets: &Assets, - perspective: [[f32; 4]; 4], - view: [[f32; 4]; 4], - options: &GameOptions - ) { - let sampler = SamplerBehavior { - minify_filter: MinifySamplerFilter::Linear, - magnify_filter: MagnifySamplerFilter::Nearest, - max_anisotropy: 8, - ..Default::default() - }; - let draw_parameters = DrawParameters { - depth: Depth { - test: DepthTest::IfLess, - write: true, - ..Default::default() - }, - polygon_mode: if options.debug_wireframe_mode { - PolygonMode::Line - } else { - PolygonMode::Fill - }, - backface_culling: glium::draw_parameters::BackfaceCullingMode::CullCounterClockwise, - ..Default::default() - }; - for (&position, chunk) in &self.chunks { - if let Some(mesh) = &chunk.mesh { - target.draw( - &mesh.vertex_buffer, - &mesh.index_buffer, - &programs.chunk, - &uniform! { - position_offset: (position.as_vec2() * CHUNK_SIZE as f32).to_array(), - view: view, - perspective: perspective, - tex: Sampler(&assets.textures.blocks, sampler) - }, - &draw_parameters - ).unwrap(); - } - } - } - - pub fn update_loaded_chunks(&mut self, around_position: Vec2, options: &GameOptions, display: &Display) { - let render_dist = options.render_distance as i32 + 1; - let inside_chunk = (around_position / CHUNK_SIZE as f32).as_ivec2(); - - //Mark all chunks for unload - for (_, chunk) in &mut self.chunks { - chunk.desired = ChunkState::Unload; - } - - //Load new/update chunks in range - for x in -render_dist..=render_dist { - for z in -render_dist..=render_dist { - let offset = IVec2::new(x, z); - let position = inside_chunk + offset; - if !self.chunks.contains_key(&position) { - self.chunks.insert(position, Chunk::new(position)); - } - { - //we only need mutable reference here: - let chunk = self.chunks.get_mut(&position).unwrap(); - if x == -render_dist || z == -render_dist || x == render_dist || z == render_dist { - chunk.desired = ChunkState::Loaded; - } else { - chunk.desired = ChunkState::Rendered; - } - } - let chunk = self.chunks.get(&position).unwrap(); - if self.thread.task_amount() < MAX_TASKS { - if matches!(chunk.state, ChunkState::Nothing) && matches!(chunk.desired, ChunkState::Loaded | ChunkState::Rendered) { - self.thread.queue_load(position); - self.chunks.get_mut(&position).unwrap().state = ChunkState::Loading; - } else if matches!(chunk.state, ChunkState::Loaded) && matches!(chunk.desired, ChunkState::Rendered) { - let mut state_changed = false; - fn all_some<'a>(x: [Option<&'a Chunk>; 4]) -> Option<[&'a Chunk; 4]> { - Some([x[0]?, x[1]?, x[2]?, x[3]?]) - } - if let Some(neighbors) = all_some(self.chunk_neighbors(chunk.position)) { - if { - neighbors[0].block_data.is_some() && - neighbors[1].block_data.is_some() && - neighbors[2].block_data.is_some() && - neighbors[3].block_data.is_some() - } { - self.thread.queue_mesh( - position, - chunk.block_data.clone().unwrap(), - [ - neighbors[0].block_data.clone().unwrap(), - neighbors[1].block_data.clone().unwrap(), - neighbors[2].block_data.clone().unwrap(), - neighbors[3].block_data.clone().unwrap(), - ] - ); - state_changed = true; - } - } - if state_changed { - self.chunks.get_mut(&position).unwrap().state = ChunkState::Rendering; - } - } - } - } - } - //Unloads and state downgrades - self.chunks.retain(|_, chunk| { - match chunk.desired { - // Chunk unload - ChunkState::Unload => false, - // Any => Nothing downgrade - ChunkState::Nothing => { - chunk.block_data = None; - chunk.mesh = None; - chunk.state = ChunkState::Nothing; - true - }, - //Render => Loaded downgrade - ChunkState::Loaded if matches!(chunk.state, ChunkState::Rendering | ChunkState::Rendered) => { - chunk.mesh = None; - chunk.state = ChunkState::Loaded; - true - }, - _ => true - } - }); - //Apply changes from threads - self.thread.apply_tasks(&mut self.chunks, display); - } -} diff --git a/src/game/world/chunk.rs b/src/game/world/chunk.rs deleted file mode 100644 index 481b04b..0000000 --- a/src/game/world/chunk.rs +++ /dev/null @@ -1,45 +0,0 @@ -use glam::IVec2; -use glium::{VertexBuffer, IndexBuffer}; -use crate::game::{ - blocks::Block, - shaders::chunk::Vertex as ChunkVertex -}; - -pub const CHUNK_SIZE: usize = 32; -pub const CHUNK_HEIGHT: usize = 255; - -pub enum ChunkState { - Unload, - Nothing, - Loading, - Loaded, - Rendering, - Rendered, -} - -pub type ChunkData = Box<[[[Block; CHUNK_SIZE]; CHUNK_HEIGHT]; CHUNK_SIZE]>; - -pub struct ChunkMesh { - pub is_dirty: bool, - pub vertex_buffer: VertexBuffer, - pub index_buffer: IndexBuffer, -} - -pub struct Chunk { - pub position: IVec2, - pub block_data: Option, - pub mesh: Option, - pub state: ChunkState, - pub desired: ChunkState, -} -impl Chunk { - pub fn new(position: IVec2) -> Self { - Self { - position, - block_data: None, - mesh: None, - state: ChunkState::Nothing, - desired: ChunkState::Nothing, - } - } -} diff --git a/src/game/world/thread.rs b/src/game/world/thread.rs deleted file mode 100644 index b051112..0000000 --- a/src/game/world/thread.rs +++ /dev/null @@ -1,97 +0,0 @@ -use glam::IVec2; -use glium::{Display, VertexBuffer, IndexBuffer, index::PrimitiveType}; -use std::{mem, thread::{self, JoinHandle}}; -use hashbrown::HashMap; -use super::chunk::{Chunk, ChunkData, ChunkState}; -use crate::game::{shaders::chunk::Vertex as ChunkVertex, world::chunk::ChunkMesh}; - -mod world_gen; -mod mesh_gen; - -#[derive(Default)] -pub struct WorldThreading { - //drain_filter is not stable yet so - //Options are needed here to take ownership, - //None values should never appear here! - pub load_tasks: HashMap>>, - pub mesh_tasks: HashMap, Vec)>>>, -} -impl WorldThreading { - pub fn new() -> Self { - Self::default() - } - pub fn is_done(&self) -> bool { - self.load_tasks.is_empty() && - self.mesh_tasks.is_empty() - } - pub fn task_amount(&self) -> usize { - self.load_tasks.len() + self.mesh_tasks.len() - } - pub fn queue_load(&mut self, position: IVec2) { - let handle = thread::spawn(move || { - world_gen::generate_chunk(position, 0xdead_cafe) - }); - if self.load_tasks.insert(position, Some(handle)).is_some() { - log::warn!("load: discarded {}, reason: new task started", position); - } - } - pub fn queue_mesh(&mut self, position: IVec2, chunk: ChunkData, neighbor_data: [ChunkData; 4]) { - let handle = thread::spawn(move || { - mesh_gen::generate_mesh(position, chunk, neighbor_data) - }); - if self.mesh_tasks.insert(position, Some(handle)).is_some() { - log::warn!("mesh: discarded {}, reason: new task started", position); - } - } - pub fn apply_tasks(&mut self, chunks: &mut HashMap, display: &Display) { - //LOAD TASKS - self.load_tasks.retain(|position, handle| { - if !chunks.contains_key(position) { - log::warn!("load: discarded {}, reason: chunk no longer exists", position); - return false - } - if !matches!(chunks.get(position).unwrap().desired, ChunkState::Loaded | ChunkState::Rendered) { - log::warn!("load: discarded {}, reason: state undesired", position); - return false - } - if !handle.as_ref().expect("Something went terribly wrong").is_finished() { - //task not finished yet, keep it and wait - return true - } - log::info!("load: done {}", position); - let handle = mem::take(handle).unwrap(); - let data = handle.join().unwrap(); - let chunk = chunks.get_mut(position).unwrap(); - chunk.block_data = Some(data); - chunk.state = ChunkState::Loaded; - false - }); - //MESH TASKS - self.mesh_tasks.retain(|position, handle| { - if !chunks.contains_key(position) { - log::warn!("mesh: discarded {}, reason: chunk no longer exists", position); - return false - } - if !matches!(chunks.get(position).unwrap().desired, ChunkState::Rendered) { - log::warn!("mesh: discarded {}, reason: state undesired", position); - return false - } - if !handle.as_ref().expect("Something went terribly wrong").is_finished() { - //task not finished yet, keep it and wait - return true - } - log::info!("mesh: done {}", position); - let handle = mem::take(handle).unwrap(); - let (shape, index) = handle.join().unwrap(); - let chunk = chunks.get_mut(position).unwrap(); - chunk.mesh = Some(ChunkMesh { - is_dirty: false, - vertex_buffer: VertexBuffer::new(display, &shape).expect("Failed to build VertexBuffer"), - index_buffer: IndexBuffer::new(display, PrimitiveType::TrianglesList, &index).expect("Failed to build IndexBuffer") - }); - chunk.state = ChunkState::Rendered; - false - }); - - } -} diff --git a/src/game/world/thread/mesh_gen.rs b/src/game/world/thread/mesh_gen.rs deleted file mode 100644 index ad32b11..0000000 --- a/src/game/world/thread/mesh_gen.rs +++ /dev/null @@ -1,139 +0,0 @@ -use glam::{IVec2, IVec3, Vec2, Vec3A, vec3a, vec2, ivec3}; -use strum::{EnumIter, IntoEnumIterator}; -use crate::game::{ - world::{ - POSITIVE_X_NEIGHBOR, - NEGATIVE_X_NEIGHBOR, - POSITIVE_Z_NEIGHBOR, - NEGATIVE_Z_NEIGHBOR, - chunk::{ChunkData, CHUNK_SIZE, CHUNK_HEIGHT} - }, - shaders::chunk::Vertex, - blocks::Block -}; - -#[repr(usize)] -#[derive(Clone, Copy, Debug, EnumIter)] -pub enum CubeFace { - Top = 0, - Front = 1, - Left = 2, - Right = 3, - Back = 4, - Bottom = 5, -} -const CUBE_FACE_VERTICES: [[Vec3A; 4]; 6] = [ - [vec3a(0., 1., 0.), vec3a(0., 1., 1.), vec3a(1., 1., 0.), vec3a(1., 1., 1.)], - [vec3a(0., 0., 0.), vec3a(0., 1., 0.), vec3a(1., 0., 0.), vec3a(1., 1., 0.)], - [vec3a(0., 0., 1.), vec3a(0., 1., 1.), vec3a(0., 0., 0.), vec3a(0., 1., 0.)], - [vec3a(1., 0., 0.), vec3a(1., 1., 0.), vec3a(1., 0., 1.), vec3a(1., 1., 1.)], - [vec3a(1., 0., 1.), vec3a(1., 1., 1.), vec3a(0., 0., 1.), vec3a(0., 1., 1.)], - [vec3a(0., 0., 1.), vec3a(0., 0., 0.), vec3a(1., 0., 1.), vec3a(1., 0., 0.)], -]; -const CUBE_FACE_NORMALS: [[f32; 3]; 6] = [ - [0., 1., 0.], - [0., 0., -1.], - [-1., 0., 0.], - [1., 0., 0.], - [0., 0., 1.], - [0., -1., 0.] -]; -const CUBE_FACE_INDICES: [u32; 6] = [0, 1, 2, 2, 1, 3]; -const UV_COORDS: [[f32; 2]; 4] = [ - [0., 0.], - [0., 1.], - [1., 0.], - [1., 1.], -]; - - -#[derive(Default)] -struct MeshBuilder { - vertex_buffer: Vec, - index_buffer: Vec, - idx_counter: u32, -} -impl MeshBuilder { - pub fn new() -> Self { - Self::default() - } - - pub fn add_face(&mut self, face: CubeFace, coord: IVec3, texture: u8) { - let coord = coord.as_vec3a(); - let face_index = face as usize; - - //Push vertexes - let norm = CUBE_FACE_NORMALS[face_index]; - let vert = CUBE_FACE_VERTICES[face_index]; - self.vertex_buffer.reserve(4); - for i in 0..4 { - self.vertex_buffer.push(Vertex { - position: (coord + vert[i]).to_array(), - normal: norm, - uv: UV_COORDS[i], - tex_index: texture - }); - } - - //Push indices - self.index_buffer.extend_from_slice(&CUBE_FACE_INDICES.map(|x| x + self.idx_counter)); - self.idx_counter += 4; - } - - pub fn finish(self) -> (Vec, Vec) { - (self.vertex_buffer, self.index_buffer) - } -} - -pub fn generate_mesh(position: IVec2, chunk_data: ChunkData, neighbors: [ChunkData; 4]) -> (Vec, Vec) { - let get_block = |pos: IVec3| -> Block { - if pos.x < 0 { - neighbors[NEGATIVE_X_NEIGHBOR][(CHUNK_SIZE as i32 + pos.x) as usize][pos.y as usize][pos.z as usize] - } else if pos.x >= CHUNK_SIZE as i32 { - neighbors[POSITIVE_X_NEIGHBOR][pos.x as usize - CHUNK_SIZE as usize][pos.y as usize][pos.z as usize] - } else if pos.z < 0 { - neighbors[NEGATIVE_Z_NEIGHBOR][pos.x as usize][pos.y as usize][(CHUNK_SIZE as i32 + pos.z) as usize] - } else if pos.z >= CHUNK_SIZE as i32 { - neighbors[POSITIVE_Z_NEIGHBOR][pos.x as usize][pos.y as usize][pos.z as usize - CHUNK_SIZE as usize] - } else { - chunk_data[pos.x as usize][pos.y as usize][pos.z as usize] - } - }; - - let mut builer = MeshBuilder::new(); - - for x in 0..CHUNK_SIZE { - for y in 0..CHUNK_HEIGHT { - for z in 0..CHUNK_SIZE { - let coord = ivec3(x as i32, y as i32, z as i32); - let descriptor = get_block(coord).descriptor(); - if descriptor.render.is_none() { - continue - } - for face in CubeFace::iter() { - let facing = Vec3A::from_array(CUBE_FACE_NORMALS[face as usize]).as_ivec3(); - let facing_coord = coord + facing; - let show = { - (facing_coord.y < 0) || - (facing_coord.y >= CHUNK_HEIGHT as i32) || - get_block(facing_coord).descriptor().render.is_none() - }; - if show { - let texures = descriptor.render.unwrap().1; - let block_texture = match face { - CubeFace::Top => texures.top, - CubeFace::Front => texures.front, - CubeFace::Left => texures.left, - CubeFace::Right => texures.right, - CubeFace::Back => texures.back, - CubeFace::Bottom => texures.bottom, - }; - builer.add_face(face, coord, block_texture as u8); - } - } - } - } - } - - builer.finish() -} diff --git a/src/game/world/thread/world_gen.rs b/src/game/world/thread/world_gen.rs deleted file mode 100644 index 34b979b..0000000 --- a/src/game/world/thread/world_gen.rs +++ /dev/null @@ -1,58 +0,0 @@ -use glam::{Vec2, DVec2, IVec2}; -use noise::{NoiseFn, Perlin, Simplex, Fbm, Seedable}; -use crate::game::{ - world::chunk::{ChunkData, CHUNK_SIZE, CHUNK_HEIGHT}, - blocks::Block -}; - -const HEIGHTMAP_SCALE: f64 = 0.004; -const MOUNTAINESS_SCALE: f64 = 0.0001; -const MNT_RAMP_1: f64 = 0.5; -const MNT_RAMP_2: f64 = 0.6; -const MTN_VAL_SCALE: f64 = 1.233; -const TERRAIN_HEIGHT_MIN: f64 = 60.; -const TERRAIN_HEIGHT_MAX: f64 = 80.; - -pub fn generate_chunk(position: IVec2, seed: u32) -> ChunkData { - let world_xz = position.as_vec2() * CHUNK_SIZE as f32; - let mut chunk = Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_HEIGHT]; CHUNK_SIZE]); - - //generate noises - let mut terrain_base_fbm: Fbm = Fbm::new(seed); - terrain_base_fbm.octaves = 6; - - let mut mountainess_base_fbm: Fbm = Fbm::new(seed); - mountainess_base_fbm.octaves = 4; - - //put everything together - for x in 0..CHUNK_SIZE { - for z in 0..CHUNK_SIZE { - let point = world_xz.as_dvec2() + DVec2::from_array([x as f64, z as f64]); - - let heightmap = (terrain_base_fbm.get((point * HEIGHTMAP_SCALE).to_array()) + 1.) / 2.; - let mountainess = MTN_VAL_SCALE * ((mountainess_base_fbm.get((point * MOUNTAINESS_SCALE).to_array()) + 1.) / 2.); - - //generate basic terrain - let terain_height = - ( - TERRAIN_HEIGHT_MIN + - (heightmap * TERRAIN_HEIGHT_MAX * (0.1 + 1.5 * if mountainess < MNT_RAMP_1 { - 0. - } else { - if mountainess > MNT_RAMP_2 { - 1. - } else { - (mountainess - MNT_RAMP_1) / (MNT_RAMP_2 - MNT_RAMP_1) * 1. - } - })) - ).floor() as usize; - for y in 0..terain_height { - chunk[x][y][z] = Block::Dirt; - } - chunk[x][terain_height][z] = Block::Grass; - } - } - - //return generated world - chunk -} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..7f408e9 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,84 @@ +use glam::{Vec2, DVec2}; +use glium::glutin::event::{DeviceEvent, VirtualKeyCode, ElementState}; +use hashbrown::HashSet; +use nohash_hasher::BuildNoHashHasher; +use shipyard::{AllStoragesView, Unique, View, IntoIter, UniqueViewMut, Workload, IntoWorkload, UniqueView}; +use crate::events::InputDeviceEvent; + +#[derive(Unique, Clone, Copy, Default, Debug)] +pub struct Inputs { + pub movement: Vec2, + pub look: Vec2, + pub action_a: bool, + pub action_b: bool, +} + +#[derive(Unique, Clone, Copy, Default, Debug)] +pub struct PrevInputs(pub Inputs); + +#[derive(Unique, Clone, Default, Debug)] +pub struct RawInputState { + pub keyboard_state: HashSet>, + pub button_state: [bool; 32], + pub mouse_delta: DVec2 +} + +pub fn process_events( + device_events: View, + mut input_state: UniqueViewMut, +) { + input_state.mouse_delta = DVec2::ZERO; + for event in device_events.iter() { + match event.event { + DeviceEvent::MouseMotion { delta } => { + input_state.mouse_delta = DVec2::from(delta); + }, + DeviceEvent::Key(input) => { + if let Some(keycode) = input.virtual_keycode { + match input.state { + ElementState::Pressed => input_state.keyboard_state.insert(keycode), + ElementState::Released => input_state.keyboard_state.remove(&keycode), + }; + } + }, + DeviceEvent::Button { button, state } => { + if button < 32 { + input_state.button_state[button as usize] = matches!(state, ElementState::Pressed); + } + }, + _ => () + } + } +} + +pub fn update_input_states ( + raw_inputs: UniqueView, + mut inputs: UniqueViewMut, + mut prev_inputs: UniqueViewMut, +) { + prev_inputs.0 = *inputs; + inputs.movement = Vec2::new( + raw_inputs.keyboard_state.contains(&VirtualKeyCode::D) as u32 as f32 - + raw_inputs.keyboard_state.contains(&VirtualKeyCode::A) as u32 as f32, + raw_inputs.keyboard_state.contains(&VirtualKeyCode::W) as u32 as f32 - + raw_inputs.keyboard_state.contains(&VirtualKeyCode::S) as u32 as f32 + ).normalize_or_zero(); + inputs.look = raw_inputs.mouse_delta.as_vec2(); + inputs.action_a = raw_inputs.button_state[1]; + inputs.action_b = raw_inputs.button_state[3]; +} + +pub fn init_input ( + storages: AllStoragesView +) { + storages.add_unique(Inputs::default()); + storages.add_unique(PrevInputs::default()); + storages.add_unique(RawInputState::default()); +} + +pub fn process_inputs() -> Workload { + ( + process_events, + update_input_states + ).into_workload() +} diff --git a/src/main.rs b/src/main.rs index 08de79c..2cda345 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,159 @@ -mod game; +use shipyard::{ + World, Workload, IntoWorkload, + UniqueView, UniqueViewMut, + NonSendSync, Unique +}; +use glium::{ + glutin::{ + event_loop::{EventLoop, ControlFlow}, + event::{Event, WindowEvent} + } +}; +use glam::vec3; +use std::time::{Instant, Duration}; + mod logging; +pub(crate) mod rendering; +pub(crate) mod world; +pub(crate) mod player; +pub(crate) mod prefabs; +pub(crate) mod transform; +pub(crate) mod settings; +pub(crate) mod camera; +pub(crate) mod events; +pub(crate) mod input; +pub(crate) mod fly_controller; +pub(crate) mod block_placement; + +use rendering::{ + Renderer, + RenderTarget, + BackgroundColor, + clear_background +}; +use world::{ + init_game_world, + loading::update_loaded_world_around_player, + raycast::update_raycasts +}; +use player::spawn_player; +use prefabs::load_prefabs; +use settings::GameSettings; +use camera::compute_cameras; +use events::{clear_events, process_glutin_events}; +use input::{init_input, process_inputs}; +use fly_controller::update_controllers; +use rendering::{ + selection_box::render_selection_box, + world::draw_world, +}; +use block_placement::block_placement_system; + +#[derive(Unique)] +pub(crate) struct DeltaTime(Duration); + +fn startup() -> Workload { + ( + load_prefabs, + init_input, + init_game_world, + spawn_player, + ).into_workload() +} +fn update() -> Workload { + ( + process_inputs, + update_controllers, + update_loaded_world_around_player, + update_raycasts, + block_placement_system, + compute_cameras + ).into_workload() +} +fn render() -> Workload { + ( + clear_background, + draw_world, + render_selection_box, + ).into_sequential_workload() +} +fn after_frame_end() -> Workload { + ( + clear_events, + ).into_workload() +} + fn main() { logging::init(); - game::run(); + + //Create event loop + let event_loop = EventLoop::new(); + + //Create a shipyard world + let mut world = World::new(); + + //Add systems and uniques, Init and load things + world.add_unique_non_send_sync(Renderer::init(&event_loop)); + world.add_unique(BackgroundColor(vec3(0.5, 0.5, 1.))); + world.add_unique(DeltaTime(Duration::default())); + world.add_unique(GameSettings::default()); + + //Register workloads + world.add_workload(startup); + world.add_workload(update); + world.add_workload(render); + world.add_workload(after_frame_end); + + //Run startup systems + world.run_workload(startup).unwrap(); + + //Run the event loop + let mut last_update = Instant::now(); + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Poll; + process_glutin_events(&mut world, &event); + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::Resized(_size) => { + // todo ... + } + WindowEvent::CloseRequested => { + log::info!("exit requested"); + *control_flow = ControlFlow::Exit; + }, + _ => (), + }, + Event::MainEventsCleared => { + //Update delta time (maybe move this into a system?) + { + let mut dt_view = world.borrow::>().unwrap(); + let now = Instant::now(); + dt_view.0 = now - last_update; + last_update = now; + } + + //Run update workflow + world.run_workload(update).unwrap(); + + //Start rendering (maybe use custom views for this?) + let target = { + let renderer = world.borrow::>>().unwrap(); + renderer.display.draw() + }; + world.add_unique_non_send_sync(RenderTarget(target)); + + //Run render workflow + world.run_workload(render).unwrap(); + + //Finish rendering + let target = world.remove_unique::().unwrap(); + target.0.finish().unwrap(); + + //FrameEnd + world.run_workload(after_frame_end).unwrap(); + }, + _ => (), + }; + }); } diff --git a/src/player.rs b/src/player.rs new file mode 100644 index 0000000..a6afaed --- /dev/null +++ b/src/player.rs @@ -0,0 +1,27 @@ +use shipyard::{Component, AllStoragesViewMut}; +use crate::{ + transform::Transform, + camera::Camera, + fly_controller::FlyController, + world::raycast::LookingAtBlock, +}; + +#[derive(Component)] +pub struct Player; + +#[derive(Component)] +pub struct MainPlayer; + +pub fn spawn_player ( + mut storages: AllStoragesViewMut +) { + log::info!("spawning player"); + storages.add_entity(( + Player, + MainPlayer, + Transform::default(), + Camera::default(), + FlyController, + LookingAtBlock::default(), + )); +} diff --git a/src/prefabs.rs b/src/prefabs.rs new file mode 100644 index 0000000..793966e --- /dev/null +++ b/src/prefabs.rs @@ -0,0 +1,87 @@ +use shipyard::{NonSendSync, UniqueView, Unique, AllStoragesView}; +use glium::{texture::{SrgbTexture2dArray, MipmapsOption}, Program}; +use strum::EnumIter; +use crate::rendering::Renderer; + +mod texture; +mod shaders; + +use texture::load_texture2darray_prefab; +use shaders::include_shader_prefab; + +pub trait AssetPaths { + fn file_name(self) -> &'static str; +} + +#[derive(Clone, Copy, Debug, EnumIter)] +#[repr(u8)] +pub enum BlockTexture { + Stone = 0, + Dirt = 1, + GrassTop = 2, + GrassSide = 3, + Sand = 4, + Bedrock = 5, + Wood = 6, + WoodTop = 7, + Leaf = 8, + Torch = 9, + TallGrass = 10, + Snow = 11, + GrassSideSnow = 12, +} +impl AssetPaths for BlockTexture { + fn file_name(self) -> &'static str { + match self { + Self::Stone => "stone.png", + Self::Dirt => "dirt.png", + Self::GrassTop => "grass_top.png", + Self::GrassSide => "grass_side.png", + Self::Sand => "sand.png", + Self::Bedrock => "bedrock.png", + Self::Wood => "wood.png", + Self::WoodTop => "wood_top.png", + Self::Leaf => "leaf.png", + Self::Torch => "torch.png", + Self::TallGrass => "tall_grass.png", + Self::Snow => "snow.png", + Self::GrassSideSnow => "grass_side_snow.png", + } + } +} + +#[derive(Unique)] +pub struct BlockTexturesPrefab(pub SrgbTexture2dArray); + +#[derive(Unique)] +pub struct ChunkShaderPrefab(pub Program); + +#[derive(Unique)] +pub struct SelBoxShaderPrefab(pub Program); + +pub fn load_prefabs( + storages: AllStoragesView, + renderer: NonSendSync> +) { + storages.add_unique_non_send_sync(BlockTexturesPrefab( + load_texture2darray_prefab::( + "./assets/blocks/".into(), + &renderer.display, + MipmapsOption::AutoGeneratedMipmaps + ) + )); + storages.add_unique_non_send_sync(ChunkShaderPrefab( + include_shader_prefab!( + "../shaders/world.vert", + "../shaders/world.frag", + &renderer.display + ) + )); + storages.add_unique_non_send_sync(SelBoxShaderPrefab( + include_shader_prefab!( + "../shaders/selection_box.vert", + "../shaders/selection_box.frag", + &renderer.display + ) + )); +} diff --git a/src/prefabs/shaders.rs b/src/prefabs/shaders.rs new file mode 100644 index 0000000..97bffa6 --- /dev/null +++ b/src/prefabs/shaders.rs @@ -0,0 +1,31 @@ + + +macro_rules! include_shader_prefab { + ($vert: literal, $frag: literal, $geom: literal, $facade: expr) => { + { + use ::glium::Program; + log::info!("↓↓↓ compiling shader prefab ↓↓↓"); + log::info!("{} {} {}", $vert, $frag, $geom); + Program::from_source( + &*$facade, + include_str!($vert), + include_str!($frag), + Some(include_str!($geom)), + ).expect("Failed to compile gpu program") + } + }; + ($vert: literal, $frag: literal, $facade: expr) => { + { + use ::glium::Program; + log::info!("↓↓↓ compiling shader prefab ↓↓↓"); + log::info!("{} {}", $vert, $frag); + Program::from_source( + &*$facade, + include_str!($vert), + include_str!($frag), + None, + ).expect("Failed to compile gpu program") + } + }; +} +pub(crate) use include_shader_prefab; diff --git a/src/prefabs/texture.rs b/src/prefabs/texture.rs new file mode 100644 index 0000000..cf2ae4c --- /dev/null +++ b/src/prefabs/texture.rs @@ -0,0 +1,44 @@ +use strum::IntoEnumIterator; +use rayon::prelude::*; +use std::{fs::File, path::PathBuf, io::BufReader}; +use glium::{texture::{SrgbTexture2dArray, RawImage2d, MipmapsOption}, backend::Facade}; +use super::AssetPaths; + +pub fn load_texture2darray_prefab< + T: AssetPaths + IntoEnumIterator, + E: Facade +>( + directory: PathBuf, + facade: &E, + mipmaps: MipmapsOption, +) -> SrgbTexture2dArray { + log::info!("↓↓↓ loading textures {} ↓↓↓", directory.as_os_str().to_str().unwrap()); + //Load raw images + let tex_files: Vec<&'static str> = T::iter().map(|x| x.file_name()).collect(); + let raw_images: Vec> = tex_files.par_iter().map(|&file_name| { + log::info!("loading texture {}", file_name); + //Get path to the image and open the file + let reader = { + let path = directory.join(file_name); + BufReader::new(File::open(path).expect("Failed to open texture file")) + }; + //Parse image data + let (image_data, dimensions) = { + let image =image::load( + reader, + image::ImageFormat::Png + ).unwrap().to_rgba8(); + let dimensions = image.dimensions(); + (image.into_raw(), dimensions) + }; + //Create a glium RawImage + RawImage2d::from_raw_rgba_reversed( + &image_data, + dimensions + ) + }).collect(); + log::info!("done loading texture files, uploading to the gpu"); + //Upload images to the GPU + SrgbTexture2dArray::with_mipmaps(facade, raw_images, mipmaps) + .expect("Failed to upload texture array to GPU") +} diff --git a/src/rendering.rs b/src/rendering.rs new file mode 100644 index 0000000..9389e76 --- /dev/null +++ b/src/rendering.rs @@ -0,0 +1,46 @@ +use shipyard::{Unique, NonSendSync, UniqueView, UniqueViewMut}; +use glium::{ + Display, Surface, + glutin::{ + event_loop::EventLoop, + window::WindowBuilder, + ContextBuilder, GlProfile + }, +}; +use glam::Vec3; + +pub mod primitives; +pub mod world; +pub mod selection_box; + +#[derive(Unique)] +pub struct RenderTarget(pub glium::Frame); + +#[derive(Unique)] +pub struct BackgroundColor(pub Vec3); + +#[derive(Unique)] +pub struct Renderer { + pub display: Display +} +impl Renderer { + pub fn init(event_loop: &EventLoop<()>) -> Self { + log::info!("initializing display"); + let wb = WindowBuilder::new() + .with_title("uwu") + .with_maximized(true); + let cb = ContextBuilder::new() + .with_depth_buffer(24) + .with_gl_profile(GlProfile::Core); + let display = Display::new(wb, cb, event_loop) + .expect("Failed to create a glium Display"); + Self { display } + } +} + +pub fn clear_background( + mut target: NonSendSync>, + color: UniqueView, +) { + target.0.clear_color_srgb_and_depth((color.0.x, color.0.y, color.0.z, 1.), 1.); +} diff --git a/src/rendering/primitives.rs b/src/rendering/primitives.rs new file mode 100644 index 0000000..7efa2e5 --- /dev/null +++ b/src/rendering/primitives.rs @@ -0,0 +1,32 @@ +pub const CUBE_VERTICES: &[f32] = &[ + // front + 0.0, 0.0, 1.0, + 1.0, 0.0, 1.0, + 1.0, 1.0, 1.0, + 0.0, 1.0, 1.0, + // back + 0.0, 0.0, 0.0, + 1.0, 0.0, 0.0, + 1.0, 1.0, 0.0, + 0.0, 1.0, 0.0 +]; +pub const CUBE_INDICES: &[u16] = &[ + // front + 0, 1, 2, + 2, 3, 0, + // right + 1, 5, 6, + 6, 2, 1, + // back + 7, 6, 5, + 5, 4, 7, + // left + 4, 0, 3, + 3, 7, 4, + // bottom + 4, 5, 1, + 1, 0, 4, + // top + 3, 2, 6, + 6, 7, 3 +]; diff --git a/src/rendering/selection_box.rs b/src/rendering/selection_box.rs new file mode 100644 index 0000000..febfe47 --- /dev/null +++ b/src/rendering/selection_box.rs @@ -0,0 +1,91 @@ +use shipyard::{View, IntoIter, NonSendSync, UniqueViewMut, UniqueView}; +use glium::{ + Surface, + implement_vertex, + IndexBuffer, + index::PrimitiveType, + VertexBuffer, uniform, + DrawParameters, + BackfaceCullingMode, + Blend, Depth, DepthTest, +}; +use crate::{ + world::raycast::LookingAtBlock, + camera::Camera, prefabs::SelBoxShaderPrefab +}; +use super::{ + RenderTarget, + primitives::{CUBE_INDICES, CUBE_VERTICES}, Renderer +}; + +#[derive(Clone, Copy, Default)] +pub struct SelBoxVertex { + pub position: [f32; 3], +} +implement_vertex!(SelBoxVertex, position); + +const fn box_vertices() -> [SelBoxVertex; CUBE_VERTICES.len() / 3] { + let mut arr = [SelBoxVertex { position: [0., 0., 0.] }; CUBE_VERTICES.len() / 3]; + let mut ptr = 0; + loop { + arr[ptr] = SelBoxVertex { + position: [ + CUBE_VERTICES[ptr * 3], + CUBE_VERTICES[(ptr * 3) + 1], + CUBE_VERTICES[(ptr * 3) + 2] + ] + }; + ptr += 1; + if ptr >= CUBE_VERTICES.len() / 3 { + return arr + } + } +} +const BOX_VERTICES: &[SelBoxVertex] = &box_vertices(); + +//wip +pub fn render_selection_box( + lookat: View, + camera: View, + mut target: NonSendSync>, + display: NonSendSync>, + program: NonSendSync>, +) { + let camera = camera.iter().next().unwrap(); + let Some(lookat) = lookat.iter().next() else { return }; + let Some(lookat) = lookat.0 else { return }; + + //this may be slow but the amount of vertices is very low + let vert = VertexBuffer::new( + &display.display, + BOX_VERTICES + ).unwrap(); + + let index = IndexBuffer::new( + &display.display, + PrimitiveType::TrianglesList, + CUBE_INDICES + ).unwrap(); + + //Darken block + target.0.draw( + &vert, + &index, + &program.0, + &uniform! { + u_color: [0., 0., 0., 0.5_f32], + u_position: lookat.block_position.as_vec3().to_array(), + perspective: camera.perspective_matrix.to_cols_array_2d(), + view: camera.view_matrix.to_cols_array_2d(), + }, + &DrawParameters { + backface_culling: BackfaceCullingMode::CullClockwise, + blend: Blend::alpha_blending(), + depth: Depth { + test: DepthTest::IfLessOrEqual, //this may be unreliable! + ..Default::default() + }, + ..Default::default() + } + ).unwrap(); +} diff --git a/src/rendering/world.rs b/src/rendering/world.rs new file mode 100644 index 0000000..2cd543c --- /dev/null +++ b/src/rendering/world.rs @@ -0,0 +1,107 @@ +use glam::Vec3; +use shipyard::{NonSendSync, UniqueView, UniqueViewMut, View, IntoIter}; +use glium::{ + implement_vertex, uniform, + Surface, DrawParameters, + uniforms::{ + Sampler, + SamplerBehavior, + MinifySamplerFilter, + MagnifySamplerFilter, + SamplerWrapFunction + }, + draw_parameters::{ + Depth, + DepthTest, + PolygonMode, + BackfaceCullingMode, + } +}; +use crate::{ + camera::Camera, + prefabs::{ + ChunkShaderPrefab, + BlockTexturesPrefab, + }, + world::{ + ChunkStorage, + ChunkMeshStorage, + chunk::CHUNK_SIZE, + }, +}; +use super::RenderTarget; + +#[derive(Clone, Copy)] +pub struct ChunkVertex { + pub position: [f32; 3], + pub normal: [f32; 3], + pub uv: [f32; 2], + pub tex_index: u8, +} +implement_vertex!(ChunkVertex, position, normal, uv, tex_index); + + +pub fn draw_world( + mut target: NonSendSync>, + chunks: UniqueView, + meshes: NonSendSync>, + program: NonSendSync>, + texture: NonSendSync>, + camera: View, +) { + let camera = camera.iter().next().expect("No cameras in the scene"); + let draw_parameters = DrawParameters { + depth: Depth { + test: DepthTest::IfLess, + write: true, + ..Default::default() + }, + polygon_mode: PolygonMode::Fill, //Change to Line for wireframe + backface_culling: BackfaceCullingMode::CullClockwise, + ..Default::default() + }; + let texture_sampler = Sampler(&texture.0, SamplerBehavior { + minify_filter: MinifySamplerFilter::LinearMipmapLinear, + magnify_filter: MagnifySamplerFilter::Nearest, + max_anisotropy: 8, + wrap_function: (SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp, SamplerWrapFunction::Clamp), + ..Default::default() + }); + let view = camera.view_matrix.to_cols_array_2d(); + let perspective = camera.perspective_matrix.to_cols_array_2d(); + + for (&position, chunk) in &chunks.chunks { + if let Some(key) = chunk.mesh_index { + let mesh = meshes.get(key).expect("Mesh index pointing to nothing"); + let world_position = position.as_vec3() * CHUNK_SIZE as f32; + + //Skip mesh if its empty + if mesh.index_buffer.len() == 0 { + continue + } + + //Frustum culling + { + let minp = world_position; + let maxp = world_position + Vec3::splat(CHUNK_SIZE as f32); + if !camera.frustum.is_box_visible(minp, maxp) { + continue + } + } + + //Draw chunk mesh + target.0.draw( + &mesh.vertex_buffer, + &mesh.index_buffer, + &program.0, + &uniform! { + position_offset: world_position.to_array(), + view: view, + perspective: perspective, + tex: texture_sampler, + }, + &draw_parameters + ).unwrap(); + } + } +} diff --git a/src/settings.rs b/src/settings.rs new file mode 100644 index 0000000..5e80b67 --- /dev/null +++ b/src/settings.rs @@ -0,0 +1,16 @@ +use shipyard::Unique; + +#[derive(Unique)] +pub struct GameSettings { + //there's a 1 chunk border of loaded but invisible around this + pub render_distance: u8, + pub mouse_sensitivity: f32, +} +impl Default for GameSettings { + fn default() -> Self { + Self { + render_distance: 5, + mouse_sensitivity: 1., + } + } +} diff --git a/src/transform.rs b/src/transform.rs new file mode 100644 index 0000000..5087b6c --- /dev/null +++ b/src/transform.rs @@ -0,0 +1,6 @@ +use shipyard::Component; +use glam::Mat4; + +#[derive(Component, Clone, Copy, Debug, Default)] +#[track(All)] +pub struct Transform(pub Mat4); diff --git a/src/world.rs b/src/world.rs new file mode 100644 index 0000000..ff75456 --- /dev/null +++ b/src/world.rs @@ -0,0 +1,111 @@ +use nohash_hasher::BuildNoHashHasher; +use shipyard::{Unique, AllStoragesView}; +use glam::IVec3; +use hashbrown::HashMap; +use anyhow::{Result, Context}; + +pub mod chunk; +pub mod block; +pub mod tasks; +pub mod loading; +pub mod mesh; +pub mod neighbors; +pub mod worldgen; +pub mod raycast; + +use chunk::{Chunk, ChunkMesh}; +use tasks::ChunkTaskManager; + +use self::{chunk::CHUNK_SIZE, block::Block}; + +//TODO separate world struct for render data +// because this is not send-sync + +#[derive(Default, Unique)] +#[track(Modification)] +pub struct ChunkStorage { + pub chunks: HashMap +} +impl ChunkStorage { + pub const fn to_chunk_coords(position: IVec3) -> (IVec3, IVec3) { + ( + IVec3::new( + position.x.div_euclid(CHUNK_SIZE as i32), + position.y.div_euclid(CHUNK_SIZE as i32), + position.z.div_euclid(CHUNK_SIZE as i32), + ), + IVec3::new( + position.x.rem_euclid(CHUNK_SIZE as i32), + position.y.rem_euclid(CHUNK_SIZE as i32), + position.z.rem_euclid(CHUNK_SIZE as i32), + ) + ) + } + pub fn get_block(&self, position: IVec3) -> Option { + let (chunk, block) = Self::to_chunk_coords(position); + let block = self.chunks + .get(&chunk)? + .block_data.as_ref()? + .blocks.get(block.x as usize)? + .get(block.y as usize)? + .get(block.z as usize)?; + Some(*block) + } + pub fn get_block_mut(&mut self, position: IVec3) -> Option<&mut Block> { + let (chunk, block) = Self::to_chunk_coords(position); + let block = self.chunks + .get_mut(&chunk)? + .block_data.as_mut()? + .blocks.get_mut(block.x as usize)? + .get_mut(block.y as usize)? + .get_mut(block.z as usize)?; + Some(block) + } + pub fn new() -> Self { + Self::default() + } +} + +#[derive(Unique)] +pub struct WorldInfo { + pub seed: u32, +} + +#[derive(Default, Unique)] +pub struct ChunkMeshStorage { + meshes: HashMap>, + index: usize, +} +impl ChunkMeshStorage { + pub fn new() -> Self { + Self { + meshes: HashMap::with_capacity_and_hasher(250, BuildNoHashHasher::default()), + index: 0, + } + } + pub fn insert(&mut self, mesh: ChunkMesh) -> usize { + let index = self.index; + self.meshes.insert_unique_unchecked(index, mesh); + self.index += 1; + index + } + pub fn update(&mut self, key: usize, mesh: ChunkMesh) -> Result<()> { + *self.meshes.get_mut(&key).context("Chunk doesn't exist")? = mesh; + Ok(()) + } + pub fn remove(&mut self, key: usize) -> Result<()> { + self.meshes.remove(&key).context("Chunk doesn't exist")?; + Ok(()) + } + pub fn get(&self, key: usize) -> Option<&ChunkMesh> { + self.meshes.get(&key) + } +} + +pub fn init_game_world( + storages: AllStoragesView, +) { + storages.add_unique_non_send_sync(ChunkMeshStorage::new()); + storages.add_unique(ChunkStorage::new()); + storages.add_unique(ChunkTaskManager::new()); +} diff --git a/src/world/block.rs b/src/world/block.rs new file mode 100644 index 0000000..92fba77 --- /dev/null +++ b/src/world/block.rs @@ -0,0 +1,105 @@ +use strum::EnumIter; +use crate::prefabs::BlockTexture; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumIter)] +#[repr(u8)] +pub enum Block { + Air, + Stone, + Dirt, + Grass, + Sand, +} +impl Block { + pub const fn descriptor(self) -> BlockDescriptor { + match self { + Self::Air => BlockDescriptor { + name: "air", + render: RenderType::None, + collision: CollisionType::None, + raycast_collision: false, + }, + Self::Stone => BlockDescriptor { + name: "stone", + render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Stone)), + collision: CollisionType::Solid, + raycast_collision: true, + }, + Self::Dirt => BlockDescriptor { + name: "dirt", + render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Dirt)), + collision: CollisionType::Solid, + raycast_collision: true, + }, + Self::Grass => BlockDescriptor { + name: "grass", + render: RenderType::SolidBlock(CubeTexture::top_sides_bottom( + BlockTexture::GrassTop, + BlockTexture::GrassSide, + BlockTexture::Dirt + )), + collision: CollisionType::Solid, + raycast_collision: true, + }, + Self::Sand => BlockDescriptor { + name: "sand", + render: RenderType::SolidBlock(CubeTexture::all(BlockTexture::Sand)), + collision: CollisionType::Solid, + raycast_collision: true, + }, + } + } +} + +#[derive(Clone, Copy, Debug)] +pub struct BlockDescriptor { + pub name: &'static str, + pub render: RenderType, + pub collision: CollisionType, + pub raycast_collision: bool, +} +// impl BlockDescriptor { +// pub fn of(block: Block) -> Self { +// block.descriptor() +// } +// } + +#[derive(Clone, Copy, Debug)] +pub struct CubeTexture { + pub top: BlockTexture, + pub bottom: BlockTexture, + pub left: BlockTexture, + pub right: BlockTexture, + pub front: BlockTexture, + pub back: BlockTexture, +} +impl CubeTexture { + pub const fn top_sides_bottom(top: BlockTexture, sides: BlockTexture, bottom: BlockTexture) -> Self { + Self { + top, + bottom, + left: sides, + right: sides, + front: sides, + back: sides, + } + } + pub const fn horizontal_vertical(horizontal: BlockTexture, vertical: BlockTexture) -> Self { + Self::top_sides_bottom(vertical, horizontal, vertical) + } + pub const fn all(texture: BlockTexture) -> Self { + Self::horizontal_vertical(texture, texture) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CollisionType { + None, + Solid, +} + +#[derive(Clone, Copy, Debug)] +pub enum RenderType { + None, + SolidBlock(CubeTexture) +} diff --git a/src/world/chunk.rs b/src/world/chunk.rs new file mode 100644 index 0000000..672d767 --- /dev/null +++ b/src/world/chunk.rs @@ -0,0 +1,65 @@ +use glam::IVec3; +use glium::{VertexBuffer, IndexBuffer}; +use super::block::Block; +use crate::rendering::world::ChunkVertex; + +pub const CHUNK_SIZE: usize = 32; + +pub type BlockData = Box<[[[Block; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]>; + +pub struct ChunkData { + pub blocks: BlockData, + //pub has_renderable_blocks: bool, +} +impl ChunkData { + // pub fn update_metadata(&mut self) { + // todo!() + // } +} + +pub struct ChunkMesh { + pub vertex_buffer: VertexBuffer, + pub index_buffer: IndexBuffer, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum CurrentChunkState { + #[default] + Nothing, + Loading, + Loaded, + CalculatingMesh, + Rendered, + RecalculatingMesh, + Unloading, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum DesiredChunkState { + #[default] + Nothing, + Loaded, + Rendered, + ToUnload, +} + +pub struct Chunk { + pub position: IVec3, + pub block_data: Option, + pub mesh_index: Option, + pub current_state: CurrentChunkState, + pub desired_state: DesiredChunkState, + pub dirty: bool, +} +impl Chunk { + pub fn new(position: IVec3) -> Self { + Self { + position, + block_data: None, + mesh_index: None, + current_state: Default::default(), + desired_state: Default::default(), + dirty: false, + } + } +} diff --git a/src/world/loading.rs b/src/world/loading.rs new file mode 100644 index 0000000..a686316 --- /dev/null +++ b/src/world/loading.rs @@ -0,0 +1,225 @@ +use glam::{IVec3, ivec3}; +use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; +use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync}; +use crate::{ + player::MainPlayer, + transform::Transform, + settings::GameSettings, + rendering::Renderer +}; +use super::{ + ChunkStorage, ChunkMeshStorage, + chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData}, + tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask}, +}; + +//todo limit task starts insted +const MAX_CHUNK_OPS: usize = 8; + +pub fn update_loaded_world_around_player() -> Workload { + ( + update_chunks_if_player_moved, + unload_downgrade_chunks, + start_required_tasks, + process_completed_tasks, + ).into_workload() +} + +pub fn update_chunks_if_player_moved( + v_settings: UniqueView, + v_local_player: View, + v_transform: View, + mut vm_world: UniqueViewMut, +) { + //Check if the player actually moved + //TODO fix this also triggers on rotation, only activate when the player crosses the chnk border + let Some((_, transform)) = (&v_local_player, v_transform.inserted_or_modified()).iter().next() else { + return + }; + + //Read game settings + let load_distance = (v_settings.render_distance + 1) as i32; + + //If it did, get it's position and current chunk + let player_position = transform.0.to_scale_rotation_translation().2; + let player_position_ivec3 = player_position.as_ivec3(); + let player_at_chunk = ivec3( + player_position_ivec3.x.div_euclid(CHUNK_SIZE as i32), + player_position_ivec3.y.div_euclid(CHUNK_SIZE as i32), + player_position_ivec3.z.div_euclid(CHUNK_SIZE as i32), + ); + + //Then, mark *ALL* chunks with ToUnload + for (_, chunk) in &mut vm_world.chunks { + chunk.desired_state = DesiredChunkState::ToUnload; + } + + //Then mark chunks that are near to the player + for x in -load_distance..=load_distance { + for y in -load_distance..=load_distance { + for z in -load_distance..=load_distance { + let chunk_pos_offset = ivec3(x, y, z); + let chunk_pos = player_at_chunk + chunk_pos_offset; + let is_border = { + chunk_pos_offset.x.abs() == load_distance || + chunk_pos_offset.y.abs() == load_distance || + chunk_pos_offset.z.abs() == load_distance + }; + //If chunk doesn't exist create it + let chunk = match vm_world.chunks.get_mut(&chunk_pos) { + Some(chunk) => chunk, + None => { + let chunk = Chunk::new(chunk_pos); + vm_world.chunks.insert_unique_unchecked(chunk_pos, chunk); + vm_world.chunks.get_mut(&chunk_pos).unwrap() + } + }; + let desired = match is_border { + true => DesiredChunkState::Loaded, + false => DesiredChunkState::Rendered, + }; + chunk.desired_state = desired; + } + } + } +} + +fn unload_downgrade_chunks( + mut vm_world: UniqueViewMut, + mut vm_meshes: NonSendSync> +) { + if !vm_world.is_modified() { + return + } + //TODO refactor this + vm_world.chunks.retain(|_, chunk| { + if chunk.desired_state == DesiredChunkState::ToUnload { + if let Some(mesh_index) = chunk.mesh_index { + vm_meshes.remove(mesh_index).unwrap(); + } + false + } else { + match chunk.desired_state { + DesiredChunkState::Loaded if matches!(chunk.current_state, CurrentChunkState::Rendered | CurrentChunkState::CalculatingMesh | CurrentChunkState::RecalculatingMesh) => { + if let Some(mesh_index) = chunk.mesh_index { + vm_meshes.remove(mesh_index).unwrap(); + } + chunk.mesh_index = None; + chunk.current_state = CurrentChunkState::Loaded; + }, + _ => (), + } + true + } + }) +} + +fn start_required_tasks( + task_manager: UniqueView, + mut world: UniqueViewMut, +) { + if !world.is_modified() { + return + } + //HACK: cant iterate over chunks.keys() or chunk directly! + let hashmap_keys: Vec = world.chunks.keys().copied().collect(); + for position in hashmap_keys { + let chunk = world.chunks.get(&position).unwrap(); + match chunk.desired_state { + DesiredChunkState::Loaded | DesiredChunkState::Rendered if chunk.current_state == CurrentChunkState::Nothing => { + //start load task + task_manager.spawn_task(ChunkTask::LoadChunk { + seed: 0xbeef_face_dead_cafe, + position + }); + //Update chunk state + let chunk = world.chunks.get_mut(&position).unwrap(); + chunk.current_state = CurrentChunkState::Loading; + // =========== + //log::trace!("Started loading chunk {position}"); + }, + DesiredChunkState::Rendered if (chunk.current_state == CurrentChunkState::Loaded || chunk.dirty) => { + //get needed data + let Some(neighbors) = world.neighbors_all(position) else { + continue + }; + let Some(data) = neighbors.mesh_data() else { + continue + }; + //spawn task + task_manager.spawn_task(ChunkTask::GenerateMesh { data, position }); + //Update chunk state + let chunk = world.chunks.get_mut(&position).unwrap(); + if chunk.dirty { + chunk.current_state = CurrentChunkState::RecalculatingMesh; + chunk.dirty = false; + } else { + chunk.current_state = CurrentChunkState::CalculatingMesh; + } + // =========== + //log::trace!("Started generating mesh for chunk {position}"); + } + _ => () + } + } +} + +fn process_completed_tasks( + task_manager: UniqueView, + mut world: UniqueViewMut, + mut meshes: NonSendSync>, + renderer: NonSendSync> +) { + for _ in 0..MAX_CHUNK_OPS { + if let Some(res) = task_manager.receive() { + match res { + ChunkTaskResponse::LoadedChunk { position, chunk_data } => { + //check if chunk exists + let Some(chunk) = world.chunks.get_mut(&position) else { + log::warn!("blocks data discarded: chunk doesn't exist"); + return + }; + + //check if chunk still wants it + if !matches!(chunk.desired_state, DesiredChunkState::Loaded | DesiredChunkState::Rendered) { + log::warn!("block data discarded: state undesirable: {:?}", chunk.desired_state); + return + } + + //set the block data + chunk.block_data = Some(ChunkData { + blocks: chunk_data + }); + + //update chunk state + chunk.current_state = CurrentChunkState::Loaded; + }, + ChunkTaskResponse::GeneratedMesh { position, vertices, indexes } => { + //check if chunk exists + let Some(chunk) = world.chunks.get_mut(&position) else { + log::warn!("mesh discarded: chunk doesn't exist"); + return + }; + + //check if chunk still wants it + if chunk.desired_state != DesiredChunkState::Rendered { + log::warn!("mesh discarded: state undesirable: {:?}", chunk.desired_state); + return + } + + //apply the mesh + let vertex_buffer = VertexBuffer::new(&renderer.display, &vertices).unwrap(); + let index_buffer = IndexBuffer::new(&renderer.display, PrimitiveType::TrianglesList, &indexes).unwrap(); + let mesh_index = meshes.insert(ChunkMesh { + vertex_buffer, + index_buffer, + }); + chunk.mesh_index = Some(mesh_index); + + //update chunk state + chunk.current_state = CurrentChunkState::Rendered; + } + } + } + } +} diff --git a/src/world/mesh.rs b/src/world/mesh.rs new file mode 100644 index 0000000..f991594 --- /dev/null +++ b/src/world/mesh.rs @@ -0,0 +1,138 @@ +use strum::{EnumIter, IntoEnumIterator}; +use glam::{Vec3A, vec3a, IVec3, ivec3}; +use std::mem::discriminant; +use super::{chunk::CHUNK_SIZE, block::{Block, RenderType}}; +use crate::rendering::world::ChunkVertex; + +pub mod data; +use data::MeshGenData; + +#[repr(usize)] +#[derive(Clone, Copy, Debug, EnumIter)] +pub enum CubeFace { + Top = 0, + Front = 1, + Left = 2, + Right = 3, + Back = 4, + Bottom = 5, +} +const CUBE_FACE_VERTICES: [[Vec3A; 4]; 6] = [ + [vec3a(0., 1., 0.), vec3a(0., 1., 1.), vec3a(1., 1., 0.), vec3a(1., 1., 1.)], + [vec3a(0., 0., 0.), vec3a(0., 1., 0.), vec3a(1., 0., 0.), vec3a(1., 1., 0.)], + [vec3a(0., 0., 1.), vec3a(0., 1., 1.), vec3a(0., 0., 0.), vec3a(0., 1., 0.)], + [vec3a(1., 0., 0.), vec3a(1., 1., 0.), vec3a(1., 0., 1.), vec3a(1., 1., 1.)], + [vec3a(1., 0., 1.), vec3a(1., 1., 1.), vec3a(0., 0., 1.), vec3a(0., 1., 1.)], + [vec3a(0., 0., 1.), vec3a(0., 0., 0.), vec3a(1., 0., 1.), vec3a(1., 0., 0.)], +]; +const CUBE_FACE_NORMALS: [Vec3A; 6] = [ + vec3a(0., 1., 0.), + vec3a(0., 0., -1.), + vec3a(-1.,0., 0.), + vec3a(1., 0., 0.), + vec3a(0., 0., 1.), + vec3a(0., -1.,0.) +]; +const CUBE_FACE_INDICES: [u32; 6] = [0, 1, 2, 2, 1, 3]; +const UV_COORDS: [[f32; 2]; 4] = [ + [0., 0.], + [0., 1.], + [1., 0.], + [1., 1.], +]; + +#[derive(Default)] +struct MeshBuilder { + vertex_buffer: Vec, + index_buffer: Vec, + idx_counter: u32, +} +impl MeshBuilder { + pub fn new() -> Self { + Self::default() + } + + pub fn add_face(&mut self, face: CubeFace, coord: IVec3, texture: u8) { + let coord = coord.as_vec3a(); + let face_index = face as usize; + + //Push vertexes + let norm = CUBE_FACE_NORMALS[face_index]; + let vert = CUBE_FACE_VERTICES[face_index]; + self.vertex_buffer.reserve(4); + for i in 0..4 { + self.vertex_buffer.push(ChunkVertex { + position: (coord + vert[i]).to_array(), + normal: norm.to_array(), + uv: UV_COORDS[i], + tex_index: texture + }); + } + + //Push indices + self.index_buffer.extend_from_slice(&CUBE_FACE_INDICES.map(|x| x + self.idx_counter)); + self.idx_counter += 4; + } + + pub fn finish(self) -> (Vec, Vec) { + (self.vertex_buffer, self.index_buffer) + } +} + +pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { + let get_block = |pos: IVec3| -> Block { + if pos.x < 0 { + data.block_data_neg_x[(CHUNK_SIZE as i32 + pos.x) as usize][pos.y as usize][pos.z as usize] + } else if pos.x >= CHUNK_SIZE as i32 { + data.block_data_pos_x[pos.x as usize - CHUNK_SIZE][pos.y as usize][pos.z as usize] + } else if pos.y < 0 { + data.block_data_neg_y[pos.x as usize][(CHUNK_SIZE as i32 + pos.y) as usize][pos.z as usize] + } else if pos.y >= CHUNK_SIZE as i32 { + data.block_data_pos_y[pos.x as usize][pos.y as usize - CHUNK_SIZE][pos.z as usize] + } else if pos.z < 0 { + data.block_data_neg_z[pos.x as usize][pos.y as usize][(CHUNK_SIZE as i32 + pos.z) as usize] + } else if pos.z >= CHUNK_SIZE as i32 { + data.block_data_pos_z[pos.x as usize][pos.y as usize][pos.z as usize - CHUNK_SIZE] + } else { + data.block_data[pos.x as usize][pos.y as usize][pos.z as usize] + } + }; + + let mut builder = MeshBuilder::new(); + + for x in 0..CHUNK_SIZE { + for y in 0..CHUNK_SIZE { + for z in 0..CHUNK_SIZE { + let coord = ivec3(x as i32, y as i32, z as i32); + let block = get_block(coord); + let descriptor = block.descriptor(); + if matches!(descriptor.render, RenderType::None) { + continue + } + for face in CubeFace::iter() { + let facing = CUBE_FACE_NORMALS[face as usize].as_ivec3(); + let facing_coord = coord + facing; + let show = discriminant(&get_block(facing_coord).descriptor().render) != discriminant(&descriptor.render); + if show { + match descriptor.render { + RenderType::SolidBlock(textures) => { + let face_texture = match face { + CubeFace::Top => textures.top, + CubeFace::Front => textures.front, + CubeFace::Left => textures.left, + CubeFace::Right => textures.right, + CubeFace::Back => textures.back, + CubeFace::Bottom => textures.bottom, + }; + builder.add_face(face, coord, face_texture as u8); + }, + _ => unimplemented!() + } + } + } + } + } + } + + builder.finish() +} diff --git a/src/world/mesh/data.rs b/src/world/mesh/data.rs new file mode 100644 index 0000000..403d1d4 --- /dev/null +++ b/src/world/mesh/data.rs @@ -0,0 +1,34 @@ +use crate::world::{ + neighbors::AllChunkNeighbors, + chunk::BlockData +}; + +pub struct MeshGenData { + pub block_data: BlockData, + pub block_data_pos_z: BlockData, + pub block_data_neg_z: BlockData, + pub block_data_pos_y: BlockData, + pub block_data_neg_y: BlockData, + pub block_data_pos_x: BlockData, + pub block_data_neg_x: BlockData, +} +impl<'a> AllChunkNeighbors<'a> { + pub fn mesh_data(&self) -> Option { + let center_block_data = self.center.block_data.as_ref()?; + let front_block_data = self.front.block_data.as_ref()?; + let back_block_data = self.back.block_data.as_ref()?; + let top_block_data = self.top.block_data.as_ref()?; + let bottom_block_data = self.bottom.block_data.as_ref()?; + let right_block_data = self.right.block_data.as_ref()?; + let left_block_data = self.left.block_data.as_ref()?; + Some(MeshGenData { + block_data: center_block_data.blocks.clone(), + block_data_pos_z: front_block_data.blocks.clone(), + block_data_neg_z: back_block_data.blocks.clone(), + block_data_pos_y: top_block_data.blocks.clone(), + block_data_neg_y: bottom_block_data.blocks.clone(), + block_data_pos_x: right_block_data.blocks.clone(), + block_data_neg_x: left_block_data.blocks.clone(), + }) + } +} diff --git a/src/world/neighbors.rs b/src/world/neighbors.rs new file mode 100644 index 0000000..5151bce --- /dev/null +++ b/src/world/neighbors.rs @@ -0,0 +1,122 @@ +use glam::{IVec3, ivec3}; +use super::chunk::Chunk; + +#[derive(Clone, Copy)] +pub struct ChunkNeighbors<'a> { + pub center: Option<&'a Chunk>, + pub top: Option<&'a Chunk>, + pub bottom: Option<&'a Chunk>, + pub left: Option<&'a Chunk>, + pub right: Option<&'a Chunk>, + pub front: Option<&'a Chunk>, + pub back: Option<&'a Chunk>, +} +#[derive(Clone, Copy)] +pub struct AllChunkNeighbors<'a> { + pub center: &'a Chunk, + pub top: &'a Chunk, + pub bottom: &'a Chunk, + pub left: &'a Chunk, + pub right: &'a Chunk, + pub front: &'a Chunk, + pub back: &'a Chunk, +} +pub struct AllChunkNeighborsMut<'a> { + pub center: &'a mut Chunk, + pub top: &'a mut Chunk, + pub bottom: &'a mut Chunk, + pub left: &'a mut Chunk, + pub right: &'a mut Chunk, + pub front: &'a mut Chunk, + pub back: &'a mut Chunk, +} + +impl<'a> ChunkNeighbors<'a> { + pub fn all(&self) -> Option> { + Some(AllChunkNeighbors { + center: self.center?, + top: self.top?, + bottom: self.bottom?, + left: self.left?, + right: self.right?, + front: self.front?, + back: self.back?, + }) + } +} +impl<'a> From> for AllChunkNeighbors<'a> { + fn from(neighbors: AllChunkNeighborsMut<'a>) -> Self { + AllChunkNeighbors { + center: neighbors.center, + top: neighbors.top, + bottom: neighbors.bottom, + left: neighbors.left, + right: neighbors.right, + front: neighbors.front, + back: neighbors.back, + } + } +} +impl<'a> From> for ChunkNeighbors<'a> { + fn from(neighbors: AllChunkNeighbors<'a>) -> Self { + ChunkNeighbors { + center: Some(neighbors.center), + top: Some(neighbors.top), + bottom: Some(neighbors.bottom), + left: Some(neighbors.left), + right: Some(neighbors.right), + front: Some(neighbors.front), + back: Some(neighbors.back), + } + } +} +impl<'a> From> for ChunkNeighbors<'a> { + fn from(neighbors: AllChunkNeighborsMut<'a>) -> Self { + ChunkNeighbors { + center: Some(neighbors.center), + top: Some(neighbors.top), + bottom: Some(neighbors.bottom), + left: Some(neighbors.left), + right: Some(neighbors.right), + front: Some(neighbors.front), + back: Some(neighbors.back), + } + } +} + +impl super::ChunkStorage { + pub fn neighbors(&self, coords: IVec3) -> ChunkNeighbors { + ChunkNeighbors { + center: self.chunks.get(&coords), + top: self.chunks.get(&(coords - ivec3(0, 1, 0))), + bottom: self.chunks.get(&(coords + ivec3(0, 1, 0))), + left: self.chunks.get(&(coords - ivec3(1, 0, 0))), + right: self.chunks.get(&(coords + ivec3(1, 0, 0))), + front: self.chunks.get(&(coords + ivec3(0, 0, 1))), + back: self.chunks.get(&(coords - ivec3(0, 0, 1))), + } + } + pub fn neighbors_all(&self, coords: IVec3) -> Option { + self.neighbors(coords).all() + } + pub fn neighbors_all_mut(&mut self, coords: IVec3) -> Option { + let [ + center, + top, + bottom, + left, + right, + front, + back + ] = self.chunks.get_many_mut([ + &coords, + &(coords - ivec3(0, 1, 0)), + &(coords + ivec3(0, 1, 0)), + &(coords - ivec3(1, 0, 0)), + &(coords + ivec3(1, 0, 0)), + &(coords + ivec3(0, 0, 1)), + &(coords - ivec3(0, 0, 1)), + ])?; + Some(AllChunkNeighborsMut { center, top, bottom, left, right, front, back }) + } +} diff --git a/src/world/raycast.rs b/src/world/raycast.rs new file mode 100644 index 0000000..3f2f407 --- /dev/null +++ b/src/world/raycast.rs @@ -0,0 +1,65 @@ +use glam::{Vec3, IVec3}; +use shipyard::{View, Component, ViewMut, IntoIter, UniqueView}; +use crate::transform::Transform; + +use super::{ChunkStorage, block::Block}; + +const RAYCAST_STEP: f32 = 0.25; + +#[derive(Clone, Copy, Debug)] +pub struct RaycastReport { + pub length: f32, + pub position: Vec3, + pub direction: Vec3, + pub block_position: IVec3, + pub block: Block, +} + +impl ChunkStorage { + //this is probably pretty slow... + pub fn raycast(&self, origin: Vec3, direction: Vec3, limit: Option) -> Option { + debug_assert!(direction.is_normalized(), "Ray direction not normalized"); + let mut position = origin; + let mut length = 0.; + loop { + let block_position = position.floor().as_ivec3(); + if let Some(block) = self.get_block(block_position) { + if block.descriptor().raycast_collision { + return Some(RaycastReport { + length, + position, + direction, + block_position, + block + }); + } + } + length += RAYCAST_STEP; + position += direction * RAYCAST_STEP; + if let Some(limit) = limit { + if length > limit { + return None; + } + } + } + } +} + +#[derive(Component, Clone, Copy, Debug, Default)] +pub struct LookingAtBlock(pub Option); + +pub fn update_raycasts( + transform: View, + mut raycast: ViewMut, + world: UniqueView, +) { + //idk if this check is even needed + if !(world.is_inserted_or_modified() || (transform.inserted_or_modified(), &raycast).iter().next().is_some()) { + return + } + for (transform, report) in (&transform, &mut raycast).iter() { + let (_, rotation, position) = transform.0.to_scale_rotation_translation(); + let direction = rotation * Vec3::NEG_Z; + *report = LookingAtBlock(world.raycast(position, direction, Some(30.))); + } +} diff --git a/src/world/tasks.rs b/src/world/tasks.rs new file mode 100644 index 0000000..2c73c16 --- /dev/null +++ b/src/world/tasks.rs @@ -0,0 +1,64 @@ +use flume::{Sender, Receiver}; +use glam::IVec3; +use shipyard::Unique; +use rayon::{ThreadPool, ThreadPoolBuilder}; +use super::{ + chunk::BlockData, + mesh::{generate_mesh, data::MeshGenData}, + worldgen::generate_world, +}; +use crate::rendering::world::ChunkVertex; + +pub enum ChunkTask { + LoadChunk { + seed: u64, + position: IVec3 + }, + GenerateMesh { + position: IVec3, + data: MeshGenData + } +} +pub enum ChunkTaskResponse { + LoadedChunk { + position: IVec3, + chunk_data: BlockData, + }, + GeneratedMesh { + position: IVec3, + vertices: Vec, + indexes: Vec + }, +} + +#[derive(Unique)] +pub struct ChunkTaskManager { + channel: (Sender, Receiver), + pool: ThreadPool, +} +impl ChunkTaskManager { + pub fn new() -> Self { + Self { + channel: flume::unbounded::(), //maybe put a bound or even bound(0)? + pool: ThreadPoolBuilder::new().num_threads(4).build().unwrap() + } + } + pub fn spawn_task(&self, task: ChunkTask) { + let sender = self.channel.0.clone(); + self.pool.spawn(move || { + let _ = sender.send(match task { + ChunkTask::GenerateMesh { position, data } => { + let (vertices, indexes) = generate_mesh(data); + ChunkTaskResponse::GeneratedMesh { position, vertices, indexes } + }, + ChunkTask::LoadChunk { position, seed } => { + let chunk_data = generate_world(position, seed); + ChunkTaskResponse::LoadedChunk { position, chunk_data } + } + }); + }); + } + pub fn receive(&self) -> Option { + self.channel.1.try_recv().ok() + } +} diff --git a/src/world/worldgen.rs b/src/world/worldgen.rs new file mode 100644 index 0000000..471055c --- /dev/null +++ b/src/world/worldgen.rs @@ -0,0 +1,49 @@ +use glam::{IVec3, ivec3}; +use bracket_noise::prelude::*; +use super::{ + chunk::{BlockData, CHUNK_SIZE}, + block::Block +}; + +pub fn generate_world(chunk_position: IVec3, seed: u64) -> BlockData { + let offset = chunk_position * CHUNK_SIZE as i32; + + let mut cave_noise = FastNoise::seeded(seed); + cave_noise.set_fractal_type(FractalType::FBM); + cave_noise.set_frequency(0.1); + + let mut dirt_noise = FastNoise::seeded(seed.rotate_left(1)); + dirt_noise.set_fractal_type(FractalType::FBM); + dirt_noise.set_frequency(0.1); + + let mut blocks = Box::new([[[Block::Air; CHUNK_SIZE]; CHUNK_SIZE]; CHUNK_SIZE]); + + if chunk_position.y >= 0 { + if chunk_position.y == 0 { + for x in 0..CHUNK_SIZE { + for z in 0..CHUNK_SIZE { + blocks[x][0][z] = Block::Dirt; + blocks[x][1][z] = Block::Grass; + } + } + } + } else { + for x in 0..CHUNK_SIZE { + for y in 0..CHUNK_SIZE { + for z in 0..CHUNK_SIZE { + let position = ivec3(x as i32, y as i32, z as i32) + offset; + let v_cave_noise = cave_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32 - 10.0).clamp(0., 1.); + let v_dirt_noise = dirt_noise.get_noise3d(position.x as f32, position.y as f32, position.z as f32) * (-position.y as f32).clamp(0., 1.); + if v_cave_noise > 0.5 { + blocks[x][y][z] = Block::Stone; + } else if v_dirt_noise > 0.5 { + blocks[x][y][z] = Block::Dirt; + } + } + } + } + } + + + blocks +}