diff --git a/assets/blocks/water.png b/assets/blocks/water.png new file mode 100644 index 0000000..35e278f Binary files /dev/null and b/assets/blocks/water.png differ diff --git a/kubi-shared/src/block.rs b/kubi-shared/src/block.rs index 73e3b18..5116241 100644 --- a/kubi-shared/src/block.rs +++ b/kubi-shared/src/block.rs @@ -22,6 +22,7 @@ pub enum BlockTexture { Cobblestone, Planks, WaterSolid, + Water, } #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, EnumIter, TryFromPrimitive)] @@ -136,7 +137,7 @@ impl Block { }, Self::Water => BlockDescriptor { name: "water", - render: RenderType::BinaryTransparency(CubeTexture::all(BlockTexture::WaterSolid)), + render: RenderType::TransBlock(CubeTexture::all(BlockTexture::Water)), collision: CollisionType::None, raycast_collision: true, drops: None, @@ -217,6 +218,7 @@ pub enum CollisionType { pub enum RenderType { None, SolidBlock(CubeTexture), + TransBlock(CubeTexture), BinaryTransparency(CubeTexture), CrossShape(CrossTexture), } diff --git a/kubi/shaders/world.frag b/kubi/shaders/world.frag index 6ff6ad4..6b8e768 100644 --- a/kubi/shaders/world.frag +++ b/kubi/shaders/world.frag @@ -1,29 +1,30 @@ -#version 300 es - -precision highp float; -precision lowp sampler2DArray; - -in vec3 v_normal; -in vec2 v_uv; -flat in uint v_tex_index; -out vec4 color; -uniform sampler2DArray tex; - -// vec4 alpha_drop(vec4 b, vec4 a) { -// if ((a.w < 1.) || (b.w < 1.)) { -// return vec4(b.xyz, 0.); -// } -// return a; -// } - -void main() { - // base color from texture - color = texture(tex, vec3(v_uv, v_tex_index)); - // discard transparent pixels - if (color.w < 0.5) discard; - //basic "lighting" - float light = abs(v_normal.x) + .8 * abs(v_normal.y) + .6 * abs(v_normal.z); - color *= vec4(vec3(light), 1.); - //discard alpha - color.w = 1.; -} +#version 300 es + +precision highp float; +precision lowp sampler2DArray; + +in vec3 v_normal; +in vec2 v_uv; +flat in uint v_tex_index; +out vec4 color; +uniform sampler2DArray tex; +uniform bool discard_alpha; + +// vec4 alpha_drop(vec4 b, vec4 a) { +// if ((a.w < 1.) || (b.w < 1.)) { +// return vec4(b.xyz, 0.); +// } +// return a; +// } + +void main() { + // base color from texture + color = texture(tex, vec3(v_uv, v_tex_index)); + // discard transparent pixels + if (color.w < (discard_alpha ? 0.01 : 0.5)) discard; + //basic "lighting" + float light = abs(v_normal.x) + .8 * abs(v_normal.y) + .6 * abs(v_normal.z); + color *= vec4(vec3(light), 1.); + //discard alpha + if (discard_alpha) color.w = 1.; +} diff --git a/kubi/src/lib.rs b/kubi/src/lib.rs index f70e889..1409870 100644 --- a/kubi/src/lib.rs +++ b/kubi/src/lib.rs @@ -15,12 +15,13 @@ use std::time::Instant; pub(crate) use kubi_shared::transform; -mod ui { - pub(crate) mod loading_screen; - pub(crate) mod connecting_screen; - pub(crate) mod chat_ui; -} -pub(crate) use ui::{loading_screen, connecting_screen, chat_ui}; +mod ui; +pub(crate) use ui::{ + loading_screen, + connecting_screen, + chat_ui, + crosshair_ui, +}; pub(crate) mod rendering; pub(crate) mod world; @@ -91,6 +92,7 @@ use filesystem::AssetManager; use client_physics::{init_client_physics, update_client_physics_late}; use chat_ui::render_chat; use chat::init_chat_manager; +use crosshair_ui::{init_crosshair_image, draw_crosshair}; /// stuff required to init the renderer and other basic systems fn pre_startup() -> Workload { @@ -116,6 +118,7 @@ fn startup() -> Workload { init_delta_time, init_client_physics, init_chat_manager, + init_crosshair_image, ).into_sequential_workload() } @@ -151,6 +154,7 @@ fn update() -> Workload { update_block_placement, apply_queued_blocks, render_chat, + draw_crosshair, ).into_sequential_workload().run_if(is_ingame), update_networking_late.run_if(is_multiplayer), compute_cameras, diff --git a/kubi/src/prefabs.rs b/kubi/src/prefabs.rs index 2eb789b..823bf1c 100644 --- a/kubi/src/prefabs.rs +++ b/kubi/src/prefabs.rs @@ -34,6 +34,7 @@ impl AssetPaths for BlockTexture { Self::Cobblestone => "cobblestone.png", Self::Planks => "planks.png", Self::WaterSolid => "solid_water.png", + Self::Water => "water.png", } } } diff --git a/kubi/src/rendering/world.rs b/kubi/src/rendering/world.rs index d922dbe..80dd02f 100644 --- a/kubi/src/rendering/world.rs +++ b/kubi/src/rendering/world.rs @@ -54,7 +54,7 @@ pub fn draw_world( settings: UniqueView ) { let camera = camera.iter().next().expect("No cameras in the scene"); - let draw_parameters = DrawParameters { + let mut draw_parameters = DrawParameters { depth: Depth { test: DepthTest::IfLess, write: true, @@ -75,6 +75,8 @@ pub fn draw_world( let view = camera.view_matrix.to_cols_array_2d(); let perspective = camera.perspective_matrix.to_cols_array_2d(); + let mut enqueue_trans = Vec::new(); + for (&position, chunk) in &chunks.chunks { if let Some(key) = chunk.mesh_index { let mesh = meshes.get(key).expect("Mesh index pointing to nothing"); @@ -107,8 +109,31 @@ pub fn draw_world( }, &draw_parameters ).unwrap(); + + if mesh.trans_index_buffer.len() > 0 { + enqueue_trans.push((chunk, mesh)); + } } } + + draw_parameters.blend = Blend::alpha_blending(); + draw_parameters.backface_culling = BackfaceCullingMode::CullingDisabled; + + for (chunk, mesh) in enqueue_trans { + let world_position = chunk.position.as_vec3() * CHUNK_SIZE as f32; + target.0.draw( + &mesh.trans_vertex_buffer, + &mesh.trans_index_buffer, + &program.0, + &uniform! { + position_offset: world_position.to_array(), + view: view, + perspective: perspective, + tex: texture_sampler, + }, + &draw_parameters + ).unwrap(); + } } pub fn draw_current_chunk_border( diff --git a/kubi/src/ui.rs b/kubi/src/ui.rs new file mode 100644 index 0000000..63c0563 --- /dev/null +++ b/kubi/src/ui.rs @@ -0,0 +1,4 @@ +pub(crate) mod loading_screen; +pub(crate) mod connecting_screen; +pub(crate) mod chat_ui; +pub(crate) mod crosshair_ui; diff --git a/kubi/src/ui/crosshair_ui.rs b/kubi/src/ui/crosshair_ui.rs new file mode 100644 index 0000000..4b7213c --- /dev/null +++ b/kubi/src/ui/crosshair_ui.rs @@ -0,0 +1,60 @@ +use std::f32::consts::PI; + +use glam::uvec2; +use hui::{ + draw::{ImageHandle, TextureFormat}, + element::{container::Container, image::Image, transformer::ElementTransformExt, UiElementExt}, + layout::Alignment, + size +}; +use shipyard::{AllStoragesViewMut, IntoIter, NonSendSync, Unique, UniqueView, UniqueViewMut, View}; +use crate::{hui_integration::UiState, player::MainPlayer, rendering::WindowSize, world::raycast::LookingAtBlock}; + +const CROSSHAIR_SIZE: usize = 9; +const CROSSHAIR: &[u8] = &[ + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, +]; + +#[derive(Unique)] +pub struct CrosshairImage(ImageHandle); + +pub fn init_crosshair_image(storages: AllStoragesViewMut) { + let mut ui = storages.borrow::>>().unwrap(); + let image = ui.hui.add_image(TextureFormat::Grayscale, CROSSHAIR, CROSSHAIR_SIZE); + storages.add_unique(CrosshairImage(image)); +} + +pub fn draw_crosshair( + mut ui: NonSendSync>, + crosshair: UniqueView, + size: UniqueView, + player: View, + raycast: View, +) { + let mut cursor_active = false; + if let Some((_, raycast)) = (&player, &raycast).iter().next() { + cursor_active = raycast.0.is_some(); + } + + Container::default() + .with_size(size!(100%)) + .with_align(Alignment::Center) + .with_children(|ui| { + Image::new(crosshair.0) + .with_color((1., 1., 1., 0.5)) + .with_size(size!(18, 18)) + .transform() + .scale(glam::Vec2::splat(if cursor_active { 1. } else { 0.66 })) + .rotate(if cursor_active { 0. } else { PI / 4. }) + .add_child(ui); + }) + .add_root(&mut ui.hui, uvec2(size.0.x & !1, size.0.y & !1).as_vec2()); +} diff --git a/kubi/src/world/chunk.rs b/kubi/src/world/chunk.rs index f22801f..07b2049 100644 --- a/kubi/src/world/chunk.rs +++ b/kubi/src/world/chunk.rs @@ -1,69 +1,71 @@ -use glam::IVec3; -use glium::{VertexBuffer, IndexBuffer}; -use crate::rendering::world::ChunkVertex; - -pub use kubi_shared::chunk::{CHUNK_SIZE, BlockData}; - -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, -} -impl DesiredChunkState { - pub fn matches_current(self, current: CurrentChunkState) -> bool { - (matches!(self, DesiredChunkState::Nothing) && matches!(current, CurrentChunkState::Nothing)) || - (matches!(self, DesiredChunkState::Loaded) && matches!(current, CurrentChunkState::Loaded)) || - (matches!(self, DesiredChunkState::Rendered) && matches!(current, CurrentChunkState::Rendered)) - } -} - -pub struct Chunk { - pub position: IVec3, - pub block_data: Option, - pub mesh_index: Option, - pub current_state: CurrentChunkState, - pub desired_state: DesiredChunkState, - pub mesh_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(), - mesh_dirty: false, - } - } -} +use glam::IVec3; +use glium::{VertexBuffer, IndexBuffer}; +use crate::rendering::world::ChunkVertex; + +pub use kubi_shared::chunk::{CHUNK_SIZE, BlockData}; + +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, + pub trans_vertex_buffer: VertexBuffer, + pub trans_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, +} +impl DesiredChunkState { + pub fn matches_current(self, current: CurrentChunkState) -> bool { + (matches!(self, DesiredChunkState::Nothing) && matches!(current, CurrentChunkState::Nothing)) || + (matches!(self, DesiredChunkState::Loaded) && matches!(current, CurrentChunkState::Loaded)) || + (matches!(self, DesiredChunkState::Rendered) && matches!(current, CurrentChunkState::Rendered)) + } +} + +pub struct Chunk { + pub position: IVec3, + pub block_data: Option, + pub mesh_index: Option, + pub current_state: CurrentChunkState, + pub desired_state: DesiredChunkState, + pub mesh_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(), + mesh_dirty: false, + } + } +} diff --git a/kubi/src/world/loading.rs b/kubi/src/world/loading.rs index efa0cce..1d4face 100644 --- a/kubi/src/world/loading.rs +++ b/kubi/src/world/loading.rs @@ -1,262 +1,267 @@ -use glam::{IVec3, ivec3}; -use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; -use kubi_shared::networking::messages::ClientToServerMessage; -use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync, track}; -use uflow::SendMode; -use crate::{ - player::MainPlayer, - transform::Transform, - settings::GameSettings, - rendering::Renderer, - state::GameState, - networking::UdpClient, -}; -use super::{ - ChunkStorage, ChunkMeshStorage, - chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData}, - tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask}, - queue::BlockUpdateQueue -}; - -const MAX_CHUNK_OPS_INGAME: usize = 6; -const MAX_CHUNK_OPS: usize = 32; - -pub fn update_loaded_world_around_player() -> Workload { - ( - update_chunks_if_player_moved, - unload_downgrade_chunks, - start_required_tasks, - process_completed_tasks, - ).into_sequential_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 chunk 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 - //TODO unsubscibe if in multiplayer - 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 udp_client: Option>, - 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 - if let Some(client) = &mut udp_client { - client.0.send( - postcard::to_allocvec(&ClientToServerMessage::ChunkSubRequest { - chunk: position, - }).unwrap().into_boxed_slice(), - 0, - SendMode::Reliable - ); - } else { - 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.mesh_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.mesh_dirty { - chunk.current_state = CurrentChunkState::RecalculatingMesh; - } else { - chunk.current_state = CurrentChunkState::CalculatingMesh; - } - chunk.mesh_dirty = false; - // =========== - //log::trace!("Started generating mesh for chunk {position}"); - } - _ => () - } - } -} - -fn process_completed_tasks( - task_manager: UniqueView, - mut world: UniqueViewMut, - mut meshes: NonSendSync>, - renderer: NonSendSync>, - state: UniqueView, - mut queue: UniqueViewMut, -) { - let mut ops: usize = 0; - while let Some(res) = task_manager.receive() { - match res { - ChunkTaskResponse::LoadedChunk { position, chunk_data, mut queued } => { - //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; - - //push queued blocks - queue.0.append(&mut queued); - drop(queued); //`queued` is empty after `append` - - //increase ops counter - ops += 1; - }, - 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::immutable(&renderer.display, &vertices).unwrap(); - let index_buffer = IndexBuffer::immutable(&renderer.display, PrimitiveType::TrianglesList, &indexes).unwrap(); - let mesh = ChunkMesh { - vertex_buffer, - index_buffer, - }; - if let Some(index) = chunk.mesh_index { - meshes.update(index, mesh).expect("Mesh update failed"); - } else { - let mesh_index = meshes.insert(mesh); - chunk.mesh_index = Some(mesh_index); - } - - //update chunk state - chunk.current_state = CurrentChunkState::Rendered; - - //increase ops counter - ops += 1; - } - } - if ops >= match *state { - GameState::InGame => MAX_CHUNK_OPS_INGAME, - _ => MAX_CHUNK_OPS, - } { break } - } -} +use glam::{IVec3, ivec3}; +use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; +use kubi_shared::networking::messages::ClientToServerMessage; +use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync, track}; +use uflow::SendMode; +use crate::{ + player::MainPlayer, + transform::Transform, + settings::GameSettings, + rendering::Renderer, + state::GameState, + networking::UdpClient, +}; +use super::{ + ChunkStorage, ChunkMeshStorage, + chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData}, + tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask}, + queue::BlockUpdateQueue +}; + +const MAX_CHUNK_OPS_INGAME: usize = 6; +const MAX_CHUNK_OPS: usize = 32; + +pub fn update_loaded_world_around_player() -> Workload { + ( + update_chunks_if_player_moved, + unload_downgrade_chunks, + start_required_tasks, + process_completed_tasks, + ).into_sequential_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 chunk 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 + //TODO unsubscibe if in multiplayer + 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 udp_client: Option>, + 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 + if let Some(client) = &mut udp_client { + client.0.send( + postcard::to_allocvec(&ClientToServerMessage::ChunkSubRequest { + chunk: position, + }).unwrap().into_boxed_slice(), + 0, + SendMode::Reliable + ); + } else { + 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.mesh_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.mesh_dirty { + chunk.current_state = CurrentChunkState::RecalculatingMesh; + } else { + chunk.current_state = CurrentChunkState::CalculatingMesh; + } + chunk.mesh_dirty = false; + // =========== + //log::trace!("Started generating mesh for chunk {position}"); + } + _ => () + } + } +} + +fn process_completed_tasks( + task_manager: UniqueView, + mut world: UniqueViewMut, + mut meshes: NonSendSync>, + renderer: NonSendSync>, + state: UniqueView, + mut queue: UniqueViewMut, +) { + let mut ops: usize = 0; + while let Some(res) = task_manager.receive() { + match res { + ChunkTaskResponse::LoadedChunk { position, chunk_data, mut queued } => { + //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; + + //push queued blocks + queue.0.append(&mut queued); + drop(queued); //`queued` is empty after `append` + + //increase ops counter + ops += 1; + }, + ChunkTaskResponse::GeneratedMesh { + position, + vertices, indices, + trans_vertices, trans_indices, + } => { + //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 + //TODO: Skip if mesh is empty? (i.e. set to None) + let mesh = ChunkMesh { + vertex_buffer: VertexBuffer::immutable(&renderer.display, &vertices).unwrap(), + index_buffer: IndexBuffer::immutable(&renderer.display, PrimitiveType::TrianglesList, &indices).unwrap(), + trans_vertex_buffer: VertexBuffer::immutable(&renderer.display, &trans_vertices).unwrap(), + trans_index_buffer: IndexBuffer::immutable(&renderer.display, PrimitiveType::TrianglesList, &trans_indices).unwrap(), + }; + if let Some(index) = chunk.mesh_index { + meshes.update(index, mesh).expect("Mesh update failed"); + } else { + let mesh_index = meshes.insert(mesh); + chunk.mesh_index = Some(mesh_index); + } + + //update chunk state + chunk.current_state = CurrentChunkState::Rendered; + + //increase ops counter + ops += 1; + } + } + if ops >= match *state { + GameState::InGame => MAX_CHUNK_OPS_INGAME, + _ => MAX_CHUNK_OPS, + } { break } + } +} diff --git a/kubi/src/world/mesh.rs b/kubi/src/world/mesh.rs index 79c2aa7..8c141ca 100644 --- a/kubi/src/world/mesh.rs +++ b/kubi/src/world/mesh.rs @@ -1,6 +1,6 @@ use glam::{IVec3, ivec3}; use strum::IntoEnumIterator; -use kubi_shared::block::{Block, RenderType}; +use kubi_shared::block::{Block, BlockTexture, RenderType}; use crate::world::chunk::CHUNK_SIZE; use crate::rendering::world::ChunkVertex; @@ -10,7 +10,10 @@ mod builder; use data::MeshGenData; use builder::{MeshBuilder, CubeFace, DiagonalFace}; -pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { +pub fn generate_mesh(data: MeshGenData) -> ( + (Vec, Vec), + (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] @@ -30,6 +33,7 @@ pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { }; let mut builder = MeshBuilder::new(); + let mut trans_builder = MeshBuilder::new(); for x in 0..CHUNK_SIZE as i32 { for y in 0..CHUNK_SIZE as i32 { @@ -39,7 +43,9 @@ pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { let descriptor = block.descriptor(); match descriptor.render { RenderType::None => continue, - RenderType::SolidBlock(textures) | RenderType::BinaryTransparency(textures) => { + RenderType::SolidBlock(textures) | + RenderType::BinaryTransparency(textures) | + RenderType::TransBlock(textures) => { for face in CubeFace::iter() { let facing_direction = face.normal(); let facing_coord = coord + facing_direction; @@ -47,10 +53,12 @@ pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { let facing_descriptor = facing_block.descriptor(); let face_obstructed = match descriptor.render { RenderType::SolidBlock(_) => matches!(facing_descriptor.render, RenderType::SolidBlock(_)), - RenderType::BinaryTransparency(_) => { + RenderType::BinaryTransparency(_) | + RenderType::TransBlock(_) => { match facing_descriptor.render { RenderType::SolidBlock(_) => true, - RenderType::BinaryTransparency(_) => block == facing_block, + RenderType::BinaryTransparency(_) | + RenderType::TransBlock(_) => block == facing_block, _ => false, } }, @@ -65,7 +73,11 @@ pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { CubeFace::Back => textures.back, CubeFace::Bottom => textures.bottom, }; - builder.add_face(face, coord, face_texture as u8); + let cur_builder = match descriptor.render { + RenderType::TransBlock(_) => &mut trans_builder, + _ => &mut builder, + }; + cur_builder.add_face(face, coord, face_texture as u8); } } }, @@ -88,5 +100,5 @@ pub fn generate_mesh(data: MeshGenData) -> (Vec, Vec) { } } - builder.finish() + (builder.finish(), trans_builder.finish()) } diff --git a/kubi/src/world/tasks.rs b/kubi/src/world/tasks.rs index 177fda3..6c22cdf 100644 --- a/kubi/src/world/tasks.rs +++ b/kubi/src/world/tasks.rs @@ -1,71 +1,80 @@ -use flume::{Sender, Receiver}; -use glam::IVec3; -use kubi_shared::queue::QueuedBlock; -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, - queued: Vec - }, - 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 add_sussy_response(&self, response: ChunkTaskResponse) { - // this WILL get stuck if the channel is bounded - // don't make the channel bounded ever - self.channel.0.send(response).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, queued) = generate_world(position, seed); - ChunkTaskResponse::LoadedChunk { position, chunk_data, queued } - } - }); - }); - } - pub fn receive(&self) -> Option { - self.channel.1.try_recv().ok() - } -} +use flume::{Sender, Receiver}; +use glam::IVec3; +use kubi_shared::queue::QueuedBlock; +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, + queued: Vec + }, + GeneratedMesh { + position: IVec3, + vertices: Vec, + indices: Vec, + trans_vertices: Vec, + trans_indices: 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 add_sussy_response(&self, response: ChunkTaskResponse) { + // this WILL get stuck if the channel is bounded + // don't make the channel bounded ever + self.channel.0.send(response).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, indices), + (trans_vertices, trans_indices), + ) = generate_mesh(data); + ChunkTaskResponse::GeneratedMesh { + position, + vertices, indices, + trans_vertices, trans_indices, + } + }, + ChunkTask::LoadChunk { position, seed } => { + let (chunk_data, queued) = generate_world(position, seed); + ChunkTaskResponse::LoadedChunk { position, chunk_data, queued } + } + }); + }); + } + pub fn receive(&self) -> Option { + self.channel.1.try_recv().ok() + } +}