From a87186860ec602c19f0d11528bef2d5123bc7e48 Mon Sep 17 00:00:00 2001 From: Lizzy Fleckenstein Date: Tue, 28 Feb 2023 18:14:06 +0100 Subject: Basic map rendering --- src/gfx.rs | 141 ++++++++++++++++++ src/gfx/map.rs | 447 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/gfx/media.rs | 42 ++++++ src/gfx/state.rs | 233 +++++++++++++++++++++++++++++ src/gfx/util.rs | 63 ++++++++ src/main.rs | 154 +++++-------------- src/net.rs | 252 +++++++++++++++++++++++++++++++ 7 files changed, 1216 insertions(+), 116 deletions(-) create mode 100644 src/gfx.rs create mode 100644 src/gfx/map.rs create mode 100644 src/gfx/media.rs create mode 100644 src/gfx/state.rs create mode 100644 src/gfx/util.rs create mode 100644 src/net.rs (limited to 'src') diff --git a/src/gfx.rs b/src/gfx.rs new file mode 100644 index 0000000..e3fb073 --- /dev/null +++ b/src/gfx.rs @@ -0,0 +1,141 @@ +use crate::{GfxEvent::*, NetEvent}; +use cgmath::Rad; +use std::time::Instant; +use tokio::sync::mpsc; +use winit::{ + event::{DeviceEvent::*, Event::*, WindowEvent::*}, + event_loop::ControlFlow::ExitWithCode, + platform::run_return::EventLoopExtRunReturn, + window::CursorGrabMode, +}; + +mod map; +mod media; +mod state; +mod util; + +pub async fn run( + mut event_loop: winit::event_loop::EventLoop, + net_events: mpsc::UnboundedSender, +) { + let window = winit::window::WindowBuilder::new() + .build(&event_loop) + .unwrap(); + + window + .set_cursor_grab(CursorGrabMode::Locked) + .or_else(|_e| window.set_cursor_grab(CursorGrabMode::Confined)) + .unwrap(); + + window.set_cursor_visible(false); + + let mut state = state::State::new(&window).await; + let mut map = None; + let mut media = media::MediaMgr::new(); + + let mut nodedefs = None; + + let mut last_frame = Instant::now(); + + event_loop.run_return(move |event, _, flow| match event { + MainEventsCleared => window.request_redraw(), + RedrawRequested(id) if id == window.id() => { + let now = Instant::now(); + let dt = now - last_frame; + last_frame = now; + + state.update(dt); + net_events + .send(NetEvent::PlayerPos( + state.camera.position.into(), + Rad(state.camera.yaw).into(), + Rad(state.camera.pitch).into(), + )) + .ok(); + + use wgpu::SurfaceError::*; + match state.render(&map) { + Ok(_) => {} + Err(Lost) => state.configure_surface(), + Err(OutOfMemory) => *flow = ExitWithCode(0), + Err(err) => eprintln!("gfx error: {err:?}"), + } + } + WindowEvent { + ref event, + window_id: id, + } if id == window.id() => match event { + CloseRequested => *flow = ExitWithCode(0), + Resized(size) => state.resize(*size), + ScaleFactorChanged { new_inner_size, .. } => state.resize(**new_inner_size), + KeyboardInput { + input: + winit::event::KeyboardInput { + virtual_keycode: Some(key), + state: key_state, + .. + }, + .. + } => { + use fps_camera::Actions; + use winit::event::{ElementState::*, VirtualKeyCode as Key}; + + let actions = match key { + Key::W => Actions::MOVE_FORWARD, + Key::A => Actions::STRAFE_LEFT, + Key::S => Actions::MOVE_BACKWARD, + Key::D => Actions::STRAFE_RIGHT, + Key::Space => Actions::FLY_UP, + Key::LShift => Actions::FLY_DOWN, + _ => Actions::empty(), + }; + + match key_state { + Pressed => state.camera.enable_actions(actions), + Released => state.camera.disable_action(actions), + } + } + _ => {} + }, + DeviceEvent { + event: MouseMotion { delta }, + .. + } => { + state.camera.update_mouse(delta.0 as f32, delta.1 as f32); + window + .set_cursor_position(winit::dpi::PhysicalPosition::new( + state.config.width / 2, + state.config.height / 2, + )) + .ok(); + } + UserEvent(event) => match event { + Close => *flow = ExitWithCode(0), + NodeDefs(defs) => nodedefs = Some(defs), + MapBlock(pos, blk) => { + if let Some(map) = map.as_mut() { + map.add_block(&mut state, pos, blk); + } + } + Media(files, finished) => { + media.add_server_media(files); + + if finished { + map = Some(map::MapRender::new( + &mut state, + &media, + nodedefs.take().unwrap_or_default(), + )); + + net_events.send(NetEvent::Ready).ok(); + } + } + PlayerPos(pos, pitch, yaw) => { + state.camera.position = pos.into(); + state.camera.pitch = Rad::::from(pitch).0; + state.camera.yaw = Rad::::from(yaw).0; + } + }, + _ => {} + }); +} diff --git a/src/gfx/map.rs b/src/gfx/map.rs new file mode 100644 index 0000000..d95a4d2 --- /dev/null +++ b/src/gfx/map.rs @@ -0,0 +1,447 @@ +use super::{media::MediaMgr, state::State, util::MatrixUniform}; +use cgmath::{prelude::*, Matrix4, Point3, Vector3}; +use mt_net::{MapBlock, NodeDef}; +use rand::Rng; +use std::{collections::HashMap, ops::Range}; +use wgpu::util::DeviceExt; + +pub struct MapRender { + pipeline: wgpu::RenderPipeline, + textures: HashMap; 2]>, + nodes: HashMap, + atlas: wgpu::BindGroup, + model: wgpu::BindGroupLayout, + blocks: HashMap<[i16; 3], BlockMesh>, +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct Vertex { + pos: [f32; 3], + tex_coords: [f32; 2], +} + +impl Vertex { + const ATTRIBS: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2]; + + fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS, + } + } +} + +struct BlockMesh { + vertex_buffer: wgpu::Buffer, + num_vertices: u32, + model: MatrixUniform, +} + +impl MapRender { + pub fn render<'a>(&'a self, state: &'a State, pass: &mut wgpu::RenderPass<'a>) { + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.atlas, &[]); + pass.set_bind_group(1, &state.camera_uniform.bind_group, &[]); + + for mesh in self.blocks.values() { + pass.set_bind_group(2, &mesh.model.bind_group, &[]); + pass.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); + pass.draw(0..mesh.num_vertices, 0..1); + } + } + + pub fn add_block(&mut self, state: &mut State, pos: Point3, block: Box) { + let mut vertices = Vec::with_capacity(10000); + for (index, content) in block.param_0.iter().enumerate() { + let def = match self.nodes.get(content) { + Some(x) => x, + None => continue, + }; + + use lerp::Lerp; + use mt_net::DrawType; + use std::array::from_fn as array; + + match def.draw_type { + DrawType::Cube => { + let pos: [i16; 3] = array(|i| ((index >> (4 * i)) & 0xf) as i16); + for (f, face) in CUBE.iter().enumerate() { + let dir = FACE_DIR[f]; + let npos: [i16; 3] = array(|i| dir[i] + pos[i]); + if npos.iter().all(|x| (0..16).contains(x)) { + let nindex = npos[0] | (npos[1] << 4) | (npos[2] << 8); + + if let Some(ndef) = self.nodes.get(&block.param_0[nindex as usize]) { + if ndef.draw_type == DrawType::Cube { + continue; + } + } + } + + let tile = &def.tiles[f]; + let rect = self.textures.get(&tile.texture).unwrap(); + + for vertex in face.iter() { + /*println!( + "{:?} {:?} {:?} {:?}", + (vertex.1[0], vertex.1[1]), + (rect[0].start, rect[1].start), + (rect[0].end, rect[1].end), + ( + vertex.1[0].lerp(rect[0].start, rect[0].end), + vertex.1[1].lerp(rect[1].start, rect[1].end) + ) + );*/ + vertices.push(Vertex { + pos: array(|i| pos[i] as f32 - 8.5 + vertex.0[i]), + tex_coords: array(|i| rect[i].start.lerp(rect[i].end, vertex.1[i])), + }) + } + } + } + DrawType::None => {} + _ => { + // TODO + } + } + } + + self.blocks.insert( + pos.into(), + BlockMesh { + vertex_buffer: state + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("mapblock.vertex_buffer"), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }), + num_vertices: vertices.len() as u32, + model: MatrixUniform::new( + &state.device, + &self.model, + Matrix4::from_translation( + pos.cast::().unwrap().to_vec() * 16.0 + Vector3::new(8.5, 8.5, 8.5), + ), + "mapblock", + false, + ), + }, + ); + } + + pub fn new(state: &mut State, media: &MediaMgr, nodes: HashMap) -> Self { + let mut rng = rand::thread_rng(); + let mut atlas_map = HashMap::new(); + let mut atlas_alloc = guillotiere::SimpleAtlasAllocator::new(guillotiere::size2(1, 1)); + + for node in nodes.values() { + let tiles = node + .tiles + .iter() + .chain(node.overlay_tiles.iter()) + .chain(node.special_tiles.iter()); + + let load_texture = |texture: &str| { + let payload = media + .get(texture) + .ok_or_else(|| format!("texture not found: {texture}"))?; + + image::load_from_memory(payload) + .or_else(|_| { + image::load_from_memory_with_format(payload, image::ImageFormat::Tga) + }) + .map_err(|e| format!("failed to load texture {texture}: {e}")) + .map(|x| image::imageops::flip_vertical(&x)) + }; + + let mut make_texture = |texture: &str| { + texture + .split('^') + .map(|part| match load_texture(part) { + Ok(v) => v, + Err(e) => { + if !texture.is_empty() && !texture.contains('[') { + eprintln!("{e}"); + } + + let mut img = image::RgbImage::new(1, 1); + rng.fill(&mut img.get_pixel_mut(0, 0).0); + + image::DynamicImage::from(img).to_rgba8() + } + }) + .reduce(|mut base, top| { + image::imageops::overlay(&mut base, &top, 0, 0); + base + }) + .unwrap() + }; + + for tile in tiles { + atlas_map.entry(tile.texture.clone()).or_insert_with(|| { + let img = make_texture(&tile.texture); + + let dimensions = img.dimensions(); + let size = guillotiere::size2(dimensions.0 as i32, dimensions.1 as i32); + + loop { + match atlas_alloc.allocate(size) { + None => { + let mut atlas_size = atlas_alloc.size(); + atlas_size.width *= 2; + atlas_size.height *= 2; + atlas_alloc.grow(atlas_size); + } + Some(v) => return (img, v), + } + } + }); + } + } + + let atlas_size = atlas_alloc.size(); + let mut atlas = image::RgbaImage::new(atlas_size.width as u32, atlas_size.height as u32); + + let textures = atlas_map + .into_iter() + .map(|(name, (img, rect))| { + let w = atlas_size.width as f32; + let h = atlas_size.height as f32; + + let x = (rect.min.x as f32 / w)..(rect.max.x as f32 / w); + let y = (rect.min.y as f32 / h)..(rect.max.y as f32 / h); + + use image::GenericImage; + atlas + .copy_from(&img, rect.min.x as u32, rect.min.y as u32) + .unwrap(); + + (name, [x, y]) + }) + .collect(); + + let size = wgpu::Extent3d { + width: atlas_size.width as u32, + height: atlas_size.height as u32, + depth_or_array_layers: 1, + }; + + let atlas_texture = state.device.create_texture(&wgpu::TextureDescriptor { + size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8UnormSrgb, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + label: Some("tile_atlas"), + view_formats: &[], + }); + + state.queue.write_texture( + wgpu::ImageCopyTexture { + texture: &atlas_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &atlas, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: std::num::NonZeroU32::new(4 * atlas_size.width as u32), + rows_per_image: std::num::NonZeroU32::new(atlas_size.height as u32), + }, + size, + ); + + let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default()); + + let atlas_sampler = state.device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + // "We've got you surrounded, stop using Nearest filter" + // - "I hate bilinear filtering I hate bilinear filtering I hate bilinear filtering" + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + }); + + let atlas_bind_group_layout = + state + .device + .create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + label: Some("atlas.bind_group_layout"), + }); + + let atlas_bind_group = state.device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &atlas_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&atlas_view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&atlas_sampler), + }, + ], + label: Some("atlas.bind_group"), + }); + + let model_bind_group_layout = MatrixUniform::layout(&state.device, "mapblock"); + + let shader = state + .device + .create_shader_module(wgpu::include_wgsl!("../../assets/shaders/map.wgsl")); + + let pipeline_layout = + state + .device + .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[ + &atlas_bind_group_layout, + &model_bind_group_layout, + &state.camera_bind_group_layout, + ], + push_constant_ranges: &[], + }); + + let pipeline = state + .device + .create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: state.config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: wgpu::CompareFunction::Less, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + + Self { + pipeline, + nodes, + textures, + atlas: atlas_bind_group, + model: model_bind_group_layout, + blocks: HashMap::new(), + } + } +} + +#[rustfmt::skip] +const CUBE: [[([f32; 3], [f32; 2]); 6]; 6] = [ + [ + ([-0.5, 0.5, -0.5], [ 0.0, 1.0]), + ([ 0.5, 0.5, 0.5], [ 1.0, 0.0]), + ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]), + ([ 0.5, 0.5, 0.5], [ 1.0, 0.0]), + ([-0.5, 0.5, -0.5], [ 0.0, 1.0]), + ([-0.5, 0.5, 0.5], [ 0.0, 0.0]), + ], + [ + ([-0.5, -0.5, -0.5], [ 0.0, 1.0]), + ([ 0.5, -0.5, -0.5], [ 1.0, 1.0]), + ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]), + ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]), + ([-0.5, -0.5, 0.5], [ 0.0, 0.0]), + ([-0.5, -0.5, -0.5], [ 0.0, 1.0]), + ], + [ + ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]), + ([ 0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([ 0.5, 0.5, -0.5], [ 0.0, 1.0]), + ([ 0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]), + ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]), + ], + [ + ([-0.5, 0.5, 0.5], [ 1.0, 1.0]), + ([-0.5, 0.5, -0.5], [ 0.0, 1.0]), + ([-0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([-0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([-0.5, -0.5, 0.5], [ 1.0, 0.0]), + ([-0.5, 0.5, 0.5], [ 1.0, 1.0]), + ], + [ + ([-0.5, -0.5, 0.5], [ 0.0, 0.0]), + ([ 0.5, -0.5, 0.5], [ 1.0, 0.0]), + ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]), + ([ 0.5, 0.5, 0.5], [ 1.0, 1.0]), + ([-0.5, 0.5, 0.5], [ 0.0, 1.0]), + ([-0.5, -0.5, 0.5], [ 0.0, 0.0]), + ], + [ + ([-0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]), + ([ 0.5, -0.5, -0.5], [ 1.0, 0.0]), + ([ 0.5, 0.5, -0.5], [ 1.0, 1.0]), + ([-0.5, -0.5, -0.5], [ 0.0, 0.0]), + ([-0.5, 0.5, -0.5], [ 0.0, 1.0]), + ], +]; + +#[rustfmt::skip] +const FACE_DIR: [[i16; 3]; 6] = [ + [ 0, 1, 0], + [ 0, -1, 0], + [ 1, 0, 0], + [-1, 0, 0], + [ 0, 0, 1], + [ 0, 0, -1], +]; diff --git a/src/gfx/media.rs b/src/gfx/media.rs new file mode 100644 index 0000000..ac5d158 --- /dev/null +++ b/src/gfx/media.rs @@ -0,0 +1,42 @@ +use std::collections::HashMap; + +#[derive(rust_embed::RustEmbed)] +#[folder = "assets/textures"] +pub struct BaseFolder; // copied from github.com/minetest/minetest + +pub struct MediaMgr { + packs: Vec>>, + srv_idx: usize, +} + +impl MediaMgr { + pub fn new() -> Self { + Self { + packs: [ + BaseFolder::iter() + .map(|file| { + ( + file.to_string(), + BaseFolder::get(&file).unwrap().data.into_owned(), + ) + }) + .collect(), + HashMap::new(), + ] + .into(), + srv_idx: 1, + } + } + + pub fn add_server_media(&mut self, files: HashMap>) { + self.packs[self.srv_idx].extend(files.into_iter()); + } + + pub fn get(&self, file: &str) -> Option<&[u8]> { + self.packs + .iter() + .rev() + .find_map(|pack| pack.get(file)) + .map(Vec::as_slice) + } +} diff --git a/src/gfx/state.rs b/src/gfx/state.rs new file mode 100644 index 0000000..9d1cfd1 --- /dev/null +++ b/src/gfx/state.rs @@ -0,0 +1,233 @@ +use super::util::MatrixUniform; +use cgmath::{prelude::*, Deg, Matrix4, Rad}; +use fps_camera::{FirstPerson, FirstPersonSettings}; +use std::time::Duration; + +pub struct State { + pub surface: wgpu::Surface, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub config: wgpu::SurfaceConfiguration, + pub fov: Rad, + pub view: Matrix4, + pub proj: Matrix4, + pub camera: FirstPerson, + pub camera_uniform: MatrixUniform, + pub camera_bind_group_layout: wgpu::BindGroupLayout, + pub depth_texture: wgpu::Texture, + pub depth_view: wgpu::TextureView, + pub depth_sampler: wgpu::Sampler, +} + +impl State { + pub async fn new(window: &winit::window::Window) -> Self { + let size = window.inner_size(); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + dx12_shader_compiler: Default::default(), + }); + + let surface = unsafe { instance.create_surface(window) }.unwrap(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: Default::default(), + label: None, + }, + None, + ) + .await + .unwrap(); + + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| f.describe().srgb) + .unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + }; + + let (depth_texture, depth_view, depth_sampler) = + Self::create_depth_texture(&config, &device); + + let camera = FirstPerson::new( + [0.0, 0.0, 0.0], + FirstPersonSettings { + speed_horizontal: 10.0, + speed_vertical: 10.0, + mouse_sensitivity_horizontal: 1.0, + mouse_sensitivity_vertical: 1.0, + }, + ); + + let camera_bind_group_layout = MatrixUniform::layout(&device, "camera"); + + let camera_uniform = MatrixUniform::new( + &device, + &camera_bind_group_layout, + Matrix4::identity(), + "camera", + true, + ); + + let mut state = Self { + surface, + device, + queue, + config, + fov: Deg(90.0).into(), + proj: Matrix4::identity(), + view: Matrix4::identity(), + camera, + camera_uniform, + camera_bind_group_layout, + depth_texture, + depth_view, + depth_sampler, + }; + + state.resize(size); + + state + } + + pub fn create_depth_texture( + config: &wgpu::SurfaceConfiguration, + device: &wgpu::Device, + ) -> (wgpu::Texture, wgpu::TextureView, wgpu::Sampler) { + let depth_size = wgpu::Extent3d { + width: config.width, + height: config.height, + depth_or_array_layers: 1, + }; + let depth_descriptor = wgpu::TextureDescriptor { + label: Some("depth texture"), + size: depth_size, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Depth32Float, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT // 3. + | wgpu::TextureUsages::TEXTURE_BINDING, + view_formats: &[], + }; + let depth_texture = device.create_texture(&depth_descriptor); + + let depth_view = depth_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let depth_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Nearest, + compare: Some(wgpu::CompareFunction::LessEqual), + lod_min_clamp: 0.0, + lod_max_clamp: 100.0, + ..Default::default() + }); + + (depth_texture, depth_view, depth_sampler) + } + + pub fn resize(&mut self, size: winit::dpi::PhysicalSize) { + if size.width > 0 && size.height > 0 { + self.config.width = size.width; + self.config.height = size.height; + self.configure_surface(); + self.update_projection(); + (self.depth_texture, self.depth_view, self.depth_sampler) = + Self::create_depth_texture(&self.config, &self.device); + } + } + + pub fn configure_surface(&mut self) { + self.surface.configure(&self.device, &self.config); + } + + pub fn update_projection(&mut self) { + self.proj = cgmath::perspective( + self.fov, + self.config.width as f32 / self.config.height as f32, + 0.1, + 100000.0, + ); + } + + pub fn update(&mut self, dt: Duration) { + let cam = self.camera.camera(dt.as_secs_f32()); + self.camera.position = cam.position; + self.view = Matrix4::from(cam.orthogonal()); + + self.camera_uniform.set(&self.queue, self.proj * self.view); + } + + pub fn render(&self, map: &Option) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0x87 as f64 / 255.0, + g: 0xCE as f64 / 255.0, + b: 0xEB as f64 / 255.0, + a: 1.0, + }), + store: true, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Clear(1.0), + store: true, + }), + stencil_ops: None, + }), + }); + + if let Some(map) = map.as_ref() { + map.render(self, &mut render_pass); + } + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } +} diff --git a/src/gfx/util.rs b/src/gfx/util.rs new file mode 100644 index 0000000..f93accd --- /dev/null +++ b/src/gfx/util.rs @@ -0,0 +1,63 @@ +use cgmath::Matrix4; +use wgpu::util::DeviceExt; + +pub struct MatrixUniform { + buffer: wgpu::Buffer, + pub bind_group: wgpu::BindGroup, +} + +impl MatrixUniform { + pub fn new( + device: &wgpu::Device, + bind_group_layout: &wgpu::BindGroupLayout, + init: Matrix4, + name: &str, + writable: bool, + ) -> Self { + let uniform: [[f32; 4]; 4] = init.into(); + + let mut usage = wgpu::BufferUsages::UNIFORM; + + if writable { + usage |= wgpu::BufferUsages::COPY_DST; + } + + let buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{name}.buffer")), + contents: bytemuck::cast_slice(&[uniform]), + usage, + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + label: Some(&format!("{name}.bind_group")), + }); + + Self { buffer, bind_group } + } + + pub fn layout(device: &wgpu::Device, name: &str) -> wgpu::BindGroupLayout { + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + label: Some(&format!("{name}.bind_group_layout")), + }) + } + + pub fn set(&self, queue: &wgpu::Queue, to: Matrix4) { + let uniform: [[f32; 4]; 4] = to.into(); + queue.write_buffer(&self.buffer, 0, bytemuck::cast_slice(&[uniform])); + } +} diff --git a/src/main.rs b/src/main.rs index 5b8817e..f517ee2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,124 +1,46 @@ -use mt_net::{MtReceiver, MtSender, RemoteSrv, ToCltPkt, ToSrvPkt}; -use rand::RngCore; -use sha2::Sha256; -use srp::{client::SrpClient, groups::G_2048}; -use std::time::Duration; -use tokio::sync::oneshot; - -async fn handle(tx: MtSender, rx: &mut MtReceiver) { - let mut username = "hydra".to_string(); - let password = "password"; - - let (init_tx, mut init_rx) = oneshot::channel(); - - { - let tx = tx.clone(); - let pkt = ToSrvPkt::Init { - serialize_version: 29, - min_proto_version: 40, - max_proto_version: 40, - player_name: username.clone(), - send_full_item_meta: false, - }; - - tokio::spawn(async move { - let mut interval = tokio::time::interval(Duration::from_millis(100)); - while tokio::select! { - _ = &mut init_rx => false, - _ = interval.tick() => true, - } { - tx.send(&pkt).await.unwrap() - } - }); - } - - let mut init_tx = Some(init_tx); - let mut auth = None; - - while let Some(res) = rx.recv().await { - match res { - Ok(pkt) => { - use ToCltPkt::*; - - match pkt { - Hello { - auth_methods, - username: name, - .. - } => { - use mt_net::AuthMethod; - - if let Some(chan) = init_tx.take() { - chan.send(()).unwrap(); - - let client = SrpClient::::new(&G_2048); - - let mut rand_bytes = vec![0; 32]; - rand::thread_rng().fill_bytes(&mut rand_bytes); - - username = name; +mod gfx; +mod net; + +use cgmath::{Deg, Point3}; +use std::collections::HashMap; +use tokio::sync::mpsc; + +#[derive(Debug, Clone)] +pub enum GfxEvent { + Close, + Media(HashMap>, bool), + NodeDefs(HashMap), + MapBlock(Point3, Box), + PlayerPos(Point3, Deg, Deg), +} - if auth_methods.contains(AuthMethod::FirstSrp) { - let verifier = client.compute_verifier( - username.to_lowercase().as_bytes(), - password.as_bytes(), - &rand_bytes, - ); +#[derive(Debug, Clone)] +pub enum NetEvent { + PlayerPos(Point3, Deg, Deg), + Ready, +} - tx.send(&ToSrvPkt::FirstSrp { - salt: rand_bytes, - verifier, - empty_passwd: password.is_empty(), - }) - .await - .unwrap(); - } else if auth_methods.contains(AuthMethod::Srp) { - let a = client.compute_public_ephemeral(&rand_bytes); - auth = Some((rand_bytes, client)); +fn main() { + println!(include_str!("../assets/ascii-art.txt")); + println!("Early WIP. Expext breakage. Trans rights <3"); - tx.send(&ToSrvPkt::SrpBytesA { a, no_sha1: true }) - .await - .unwrap(); - } else { - panic!("unsupported auth methods: {auth_methods:?}"); - } - } - } - SrpBytesSaltB { salt, b } => { - if let Some((a, client)) = auth.take() { - let m = client - .process_reply( - &a, - username.to_lowercase().as_bytes(), - password.as_bytes(), - &salt, - &b, - ) - .unwrap() - .proof() - .into(); + let (net_tx, net_rx) = mpsc::unbounded_channel(); + let event_loop = winit::event_loop::EventLoopBuilder::::with_user_event().build(); + let event_loop_proxy = event_loop.create_proxy(); - tx.send(&ToSrvPkt::SrpBytesM { m }).await.unwrap(); - } - } - x => println!("{x:?}"), - } - } - Err(err) => eprintln!("{err}"), - } - } -} + let runtime = tokio::runtime::Builder::new_multi_thread() + .enable_io() + .enable_time() + .thread_name("network") + .build() + .unwrap(); -#[tokio::main] -async fn main() { - let (tx, mut rx) = mt_net::connect("localhost:30000").await.unwrap(); + let net_thread = runtime.spawn(net::run(event_loop_proxy, net_rx)); - tokio::select! { - _ = tokio::signal::ctrl_c() => println!("canceled"), - _ = handle(tx, &mut rx) => { - println!("disconnected"); - } - } + // graphics code is pseudo async: the winit event loop is blocking + // so we can't really use async capabilities + futures::executor::block_on(gfx::run(event_loop, net_tx)); - rx.close().await; + // wait for net to finish + runtime.block_on(net_thread).unwrap(); } diff --git a/src/net.rs b/src/net.rs new file mode 100644 index 0000000..323fe06 --- /dev/null +++ b/src/net.rs @@ -0,0 +1,252 @@ +use crate::{GfxEvent, NetEvent}; +use cgmath::{Deg, Point3, Vector3}; +use futures::future::OptionFuture; +use mt_net::{CltSender, ReceiverExt, SenderExt, ToCltPkt, ToSrvPkt}; +use rand::RngCore; +use sha2::Sha256; +use srp::{client::SrpClient, groups::G_2048}; +use std::{future::Future, time::Duration}; +use tokio::{ + sync::mpsc, + time::{interval, Instant, Interval}, +}; +use winit::event_loop::EventLoopProxy; + +enum AuthState { + Init(Interval), + Verify(Vec, SrpClient<'static, Sha256>), + Done, +} + +struct Conn { + tx: CltSender, + auth: AuthState, + send_pos_iv: Option, + username: String, + password: String, + pos: Point3, + pitch: Deg, + yaw: Deg, + events: EventLoopProxy, +} + +fn maybe_tick(iv: Option<&mut Interval>) -> OptionFuture + '_> { + OptionFuture::from(iv.map(Interval::tick)) +} + +pub(crate) async fn run( + evt_out: EventLoopProxy, + mut evt_in: mpsc::UnboundedReceiver, +) { + let (tx, mut rx, worker) = mt_net::connect("localhost:30000").await.unwrap(); + + let mut conn = Conn { + tx, + auth: AuthState::Init(interval(Duration::from_millis(100))), + send_pos_iv: None, + username: "shrek".into(), // shrek is love, shrek is life <3 + password: "boobies".into(), + pos: Point3::new(0.0, 0.0, 0.0), + pitch: Deg(0.0), + yaw: Deg(0.0), + events: evt_out, + }; + + let init_pkt = ToSrvPkt::Init { + serialize_version: 29, + proto_version: 40..=40, + player_name: conn.username.clone(), + send_full_item_meta: false, + }; + + let worker_thread = tokio::spawn(worker.run()); + + loop { + tokio::select! { + pkt = rx.recv() => match pkt { + None => break, + Some(Err(e)) => eprintln!("{e}"), + Some(Ok(v)) => conn.handle_pkt(v).await, + }, + Some(_) = maybe_tick(match &mut conn.auth { + AuthState::Init(iv) => Some(iv), + _ => None, + }) => { + conn.tx.send(&init_pkt).await.unwrap(); + } + Some(_) = maybe_tick(conn.send_pos_iv.as_mut()) => { + conn.tx + .send(&ToSrvPkt::PlayerPos(mt_net::PlayerPos { + pos: conn.pos, + vel: Vector3::new(0.0, 0.0, 0.0), + pitch: conn.pitch, + yaw: conn.yaw, + keys: mt_net::enumset::EnumSet::empty(), + fov: Deg(90.0).into(), + wanted_range: 12, + })) + .await + .unwrap(); + } + evt = evt_in.recv() => { + match evt { + Some(NetEvent::PlayerPos(pos, yaw, pitch)) => { + conn.pos = pos; + conn.yaw = yaw; + conn.pitch = pitch; + }, + Some(NetEvent::Ready) => { + conn.tx + .send(&ToSrvPkt::CltReady { + major: 0, + minor: 1, + patch: 0, + reserved: 0, + version: format!("Minetest Rust {}", env!("CARGO_PKG_VERSION")), + formspec: 4, + }) + .await + .unwrap(); + } + None => conn.tx.close(), + } + } + _ = tokio::signal::ctrl_c() => { + conn.tx.close(); + } + } + } + + conn.events.send_event(GfxEvent::Close).ok(); // TODO: make sure to send this on panic + worker_thread.await.unwrap(); +} + +impl Conn { + async fn handle_pkt(&mut self, pkt: ToCltPkt) { + use ToCltPkt::*; + + match pkt { + Hello { + auth_methods, + username: name, + .. + } => { + use mt_net::AuthMethod; + + if !matches!(self.auth, AuthState::Init(_)) { + return; + } + + let srp = SrpClient::::new(&G_2048); + + let mut rand_bytes = vec![0; 32]; + rand::thread_rng().fill_bytes(&mut rand_bytes); + + if self.username != name { + panic!("username changed"); + } + + if auth_methods.contains(AuthMethod::FirstSrp) { + let verifier = srp.compute_verifier( + self.username.to_lowercase().as_bytes(), + self.password.as_bytes(), + &rand_bytes, + ); + + self.tx + .send(&ToSrvPkt::FirstSrp { + salt: rand_bytes, + verifier, + empty_passwd: self.password.is_empty(), + }) + .await + .unwrap(); + + self.auth = AuthState::Done; + } else if auth_methods.contains(AuthMethod::Srp) { + let a = srp.compute_public_ephemeral(&rand_bytes); + + self.tx + .send(&ToSrvPkt::SrpBytesA { a, no_sha1: true }) + .await + .unwrap(); + + self.auth = AuthState::Verify(rand_bytes, srp); + } else { + panic!("unsupported auth methods: {auth_methods:?}"); + } + } + SrpBytesSaltB { salt, b } => { + if let AuthState::Verify(a, srp) = &self.auth { + let m = srp + .process_reply( + a, + self.username.to_lowercase().as_bytes(), + self.password.as_bytes(), + &salt, + &b, + ) + .unwrap() + .proof() + .into(); + + self.tx.send(&ToSrvPkt::SrpBytesM { m }).await.unwrap(); + + self.auth = AuthState::Done; + } + } + NodeDefs(defs) => { + self.events.send_event(GfxEvent::NodeDefs(defs.0)).ok(); + } + Kick(reason) => { + println!("kicked: {reason}"); + } + AcceptAuth { player_pos, .. } => { + self.tx + .send(&ToSrvPkt::Init2 { + lang: "en_US".into(), // localization is unironically overrated + }) + .await + .unwrap(); + + self.pos = player_pos; + self.send_pos_iv = Some(interval(Duration::from_millis(100))); + } + MovePlayer { pos, pitch, yaw } => { + self.pos = pos; + self.pitch = pitch; + self.yaw = yaw; + + self.events + .send_event(GfxEvent::PlayerPos(self.pos, self.pitch, self.yaw)) + .ok(); + } + BlockData { pos, block } => { + self.events.send_event(GfxEvent::MapBlock(pos, block)).ok(); + self.tx + .send(&ToSrvPkt::GotBlocks { + blocks: Vec::from([pos]), + }) + .await + .unwrap(); + } + AnnounceMedia { files, .. } => { + self.tx + .send(&ToSrvPkt::RequestMedia { + filenames: files.into_keys().collect(), // TODO: cache + }) + .await + .ok(); + } + Media { files, n, i } => { + self.events + .send_event(GfxEvent::Media(files, i + 1 == n)) + .ok(); + } + ChatMsg { text, .. } => { + println!("{text}"); + } + _ => {} + } + } +} -- cgit v1.2.3