From b5ced3af46c96ceb959fbbf1addfeba3bd4f76d5 Mon Sep 17 00:00:00 2001 From: Jon Santmyer Date: Wed, 15 Apr 2026 16:53:58 -0400 Subject: first commit. working body rendering --- src/canvas.rs | 176 +++++++++++++++++++++++++++++++ src/eguictx.rs | 143 +++++++++++++++++++++++++ src/known_stars.rs | 24 +++++ src/main.rs | 178 +++++++++++++++++++++++++++++++ src/solar_system.rs | 161 ++++++++++++++++++++++++++++ src/tacmap.rs | 158 ++++++++++++++++++++++++++++ src/tacmap/camera.rs | 272 ++++++++++++++++++++++++++++++++++++++++++++++++ src/tacmap/render.rs | 233 +++++++++++++++++++++++++++++++++++++++++ src/texture.rs | 96 +++++++++++++++++ src/timeman.rs | 69 ++++++++++++ src/vertex.rs | 33 ++++++ src/wgpuctx/mod.rs | 245 +++++++++++++++++++++++++++++++++++++++++++ src/wgpuctx/pipeline.rs | 121 +++++++++++++++++++++ src/window.rs | 148 ++++++++++++++++++++++++++ src/window/ui.rs | 96 +++++++++++++++++ 15 files changed, 2153 insertions(+) create mode 100644 src/canvas.rs create mode 100644 src/eguictx.rs create mode 100644 src/known_stars.rs create mode 100644 src/main.rs create mode 100644 src/solar_system.rs create mode 100644 src/tacmap.rs create mode 100644 src/tacmap/camera.rs create mode 100644 src/tacmap/render.rs create mode 100644 src/texture.rs create mode 100644 src/timeman.rs create mode 100644 src/vertex.rs create mode 100644 src/wgpuctx/mod.rs create mode 100644 src/wgpuctx/pipeline.rs create mode 100644 src/window.rs create mode 100644 src/window/ui.rs (limited to 'src') diff --git a/src/canvas.rs b/src/canvas.rs new file mode 100644 index 0000000..dbcac7f --- /dev/null +++ b/src/canvas.rs @@ -0,0 +1,176 @@ +use wgpu::RenderPipeline; + +use crate::wgpuctx::WgpuCtx; +use crate::texture::Texture; +use crate::vertex::Vertex; +use crate::wgpuctx::pipeline::RenderPipelineBuilder; + +struct CanvasTexture +{ + pub texture: Texture, + buffer: wgpu::Buffer +} + +pub struct Canvas +{ + pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + texture: CanvasTexture, + + position: winit::dpi::LogicalPosition, + size: winit::dpi::LogicalSize +} + +impl Canvas +{ + fn create_canvas_texture( + wgpuctx: &WgpuCtx, + canvas_size: winit::dpi::LogicalSize, + window_size: winit::dpi::PhysicalSize) + -> CanvasTexture + { + let canvas_physical_size = winit::dpi::PhysicalSize::::new( + (window_size.width as f32 * canvas_size.width).floor() as u32, + (window_size.height as f32 * canvas_size.height).floor() as u32 + ); + + CanvasTexture::new(wgpuctx, canvas_physical_size) + } + + pub fn new( + wgpuctx: &WgpuCtx, + window_size: winit::dpi::PhysicalSize, + position: winit::dpi::LogicalPosition, + size: winit::dpi::LogicalSize + ) -> Result { + + let pos_x = position.x * 2.0 - 1.0; + let pos_y = position.y * 2.0 - 1.0; + let size_x = size.width * 2.0; + let size_y = size.height * 2.0; + + let vertices: &[Vertex] = &[ + Vertex { position: [ pos_x, pos_y, 0.0 ], uv: [ 0.0, 1.0 ] }, + Vertex { position: [ pos_x+size_x, pos_y, 0.0 ], uv: [ 1.0, 1.0 ] }, + Vertex { position: [ pos_x+size_x, pos_y+size_y, 0.0 ], uv: [ 1.0, 0.0 ] }, + Vertex { position: [ pos_x+size_x, pos_y+size_y, 0.0 ], uv: [ 1.0, 0.0 ] }, + Vertex { position: [ pos_x, pos_y+size_y, 0.0 ], uv: [ 0.0, 0.0 ] }, + Vertex { position: [ pos_x, pos_y, 0.0 ], uv: [ 0.0, 1.0 ] }, + ]; + + let vertex_buffer = wgpuctx.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(vertices), + usage: wgpu::BufferUsages::VERTEX + } + ); + + let shader = wgpuctx.create_shader( + wgpu::include_wgsl!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/shaders/canvas.wgsl") + )); + + let canvas_texture = Canvas::create_canvas_texture(wgpuctx, size, window_size); + + let render_pipeline = RenderPipelineBuilder::new( + &shader) + .add_bindgroup(&canvas_texture.texture.bindgrouplayout) + .add_vertex_layout(Vertex::descr()) + .build(Some("Canvas render pipleine"), wgpuctx); + + Ok(Self { + pipeline: render_pipeline, + vertex_buffer: vertex_buffer, + texture: canvas_texture, + position: position, + size: size + }) + } + + pub fn resize( + &mut self, + wgpuctx: &WgpuCtx, + width: u32, + height: u32) + { + let window_size = winit::dpi::PhysicalSize::new(width, height); + self.texture = Canvas::create_canvas_texture(wgpuctx, self.size, window_size); + } + + pub fn present( + &mut self, + screen_pass: &mut wgpu::RenderPass + ) + -> Result<(), wgpu::SurfaceError> { + + screen_pass.set_pipeline(&self.pipeline); + self.texture.texture.use_on_pass(screen_pass, 0); + screen_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + screen_pass.draw(0..6,0..1); + Ok(()) + } + + pub fn view(&self) + -> &wgpu::TextureView + { + self.texture.texture.view() + } +} //impl Canvas + +impl CanvasTexture +{ + pub fn new( + wgpuctx: &WgpuCtx, + size: winit::dpi::PhysicalSize) + -> Self + { + let u32_size = std::mem::size_of::() as u32; + + let buffer_size = (u32_size * size.width * size.height) as wgpu::BufferAddress; + let buffer_descr = wgpu::BufferDescriptor { + size: buffer_size, + usage: wgpu::BufferUsages::COPY_DST, + label: None, + mapped_at_creation: false + }; + + let texture = wgpuctx.new_render_texture(size); + let buffer = wgpuctx.create_buffer(&buffer_descr); + + Self { + texture: texture, + buffer: buffer + } + } +} + +impl WgpuCtx +{ + pub fn new_render_texture( + &self, + size: winit::dpi::PhysicalSize) + -> Texture + { + let descr = wgpu::TextureDescriptor { + size: wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1 + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + view_formats: &[ + wgpu::TextureFormat::Bgra8UnormSrgb + ], + usage: wgpu::TextureUsages::TEXTURE_BINDING | + wgpu::TextureUsages::COPY_SRC | + wgpu::TextureUsages::RENDER_ATTACHMENT, + label: None + }; + self.new_texture(descr) + } +} diff --git a/src/eguictx.rs b/src/eguictx.rs new file mode 100644 index 0000000..572d352 --- /dev/null +++ b/src/eguictx.rs @@ -0,0 +1,143 @@ +use egui::{Context}; +use egui_wgpu::Renderer; +use egui_winit::{EventResponse, State}; +use wgpu::{CommandEncoder, CommandEncoderDescriptor, Operations, RenderPassDescriptor, TextureView}; +use winit::{event::WindowEvent, window::{Theme, Window}}; + +use crate::wgpuctx::WgpuCtx; + + +pub struct EguiCtx { + state: State, + renderer: Renderer, + context: Context, + ready: bool +} + +impl EguiCtx +{ + pub fn new( + window: &winit::window::Window, + wgpuctx: &WgpuCtx) + -> Self { + let ctx = egui::Context::default(); + + let viewport_id = ctx.viewport_id(); + let state = egui_winit::State::new( + ctx.clone(), + viewport_id, + window, + Some(window.scale_factor() as _), + Some(Theme::Light), + None); + + let renderer = Renderer::new( + wgpuctx.device(), + wgpuctx.surface_config().format, + egui_wgpu::RendererOptions { + depth_stencil_format: None, + msaa_samples: 1, + ..Default::default() + } + ); + + Self { + context: ctx, + state: state, + renderer: renderer, + ready: false + } + } + + pub fn context(&self) -> &Context + { self.state.egui_ctx() } + + pub fn window_event( + &mut self, + window: &Window, + event: &WindowEvent) + -> EventResponse + { + self.state.on_window_event(window, event) + } + + pub fn prepare( + &mut self, + window: &Window) + { + let raw_input = self.state.take_egui_input(window); + self.context().begin_pass(raw_input); + self.ready = true; + } + + pub fn present( + &mut self, + window: &Window, + wgpuctx: &WgpuCtx, + encoder: &mut CommandEncoder, + view: &TextureView) + + { + if !self.ready { + return; + } + self.ready = false; + + let full_output = self.context().end_pass(); + self.state.handle_platform_output(window, full_output.platform_output); + + let jobs = self.context().tessellate( + full_output.shapes, + full_output.pixels_per_point + ); + + let screen_descriptor = { + let view_size = view.texture().size(); + let (width, height) = (view_size.width, view_size.height); + egui_wgpu::ScreenDescriptor { + size_in_pixels: [width, height], + pixels_per_point: full_output.pixels_per_point + } + }; + + for (id, image_delta) in full_output.textures_delta.set { + self.renderer.update_texture(wgpuctx.device(), wgpuctx.queue(), id, &image_delta); + } + + self.renderer.update_buffers( + wgpuctx.device(), + wgpuctx.queue(), + encoder, + &jobs, + &screen_descriptor + ); + + { + let mut render_pass = encoder.begin_render_pass(&RenderPassDescriptor { + label: Some("EguiCtx render pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: view, + resolve_target: None, + ops: Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store + }, + depth_slice: None + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None + }); + + self.renderer.render( + &mut render_pass.forget_lifetime(), + &jobs, + &screen_descriptor + ); + } + + for id in &full_output.textures_delta.free { + self.renderer.free_texture(id); + } + } +} diff --git a/src/known_stars.rs b/src/known_stars.rs new file mode 100644 index 0000000..cf78aa2 --- /dev/null +++ b/src/known_stars.rs @@ -0,0 +1,24 @@ +use phf::phf_map; +use std::fmt; +use std::error::Error; + +#[derive(Debug)] +pub struct StarNotFoundError +{ + pub star: &'static str +} + +impl fmt::Display for StarNotFoundError { + fn fmt( + &self, + f: &mut fmt::Formatter<'_>) + -> fmt::Result { + write!(f, "Star {:0} is not known!", self.star) + } +} + +impl Error for StarNotFoundError {} + +pub static KNOWN_STARS: phf::Map<&'static str, &'static str> = phf_map! { + "sol" => include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/systems/sol.csv")) +}; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b6efdcd --- /dev/null +++ b/src/main.rs @@ -0,0 +1,178 @@ +mod window; +mod tacmap; +mod wgpuctx; +mod eguictx; +mod vertex; +mod texture; +mod canvas; +mod solar_system; +mod known_stars; +mod timeman; + +use std::cell::RefCell; +use std::thread; +use std::time::{Duration, Instant}; + +use winit::event::{KeyEvent, WindowEvent}; +use winit::event_loop::{EventLoop, ActiveEventLoop, ControlFlow}; +use winit::error::{EventLoopError}; +use winit::application::{ApplicationHandler}; +use winit::keyboard::PhysicalKey; +use winit::window::{Window, WindowId, WindowAttributes}; + +use solar_system::SolarSystem; + +use crate::timeman::TimeMan; +use crate::window::GameWindow; + +const TARGET_FPS: f32 = 100.0; +const TARGET_DT: Duration = Duration::from_millis((1000.0 / TARGET_FPS) as u64); + +struct SystemicApp +{ + window: Option, + wgpu_instance: wgpu::Instance, + + last_render_time: Instant, + game_state: Option> +} + +struct GameState +{ + timeman: TimeMan, + solar_systems: Vec +} + +impl SystemicApp +{ + fn create_window( + &mut self, + event_loop: &ActiveEventLoop) + { + self.window = Some(GameWindow::new( + &self.wgpu_instance, + event_loop).unwrap()); + + self.game_state = Some(RefCell::new(GameState::new())); + } +} + +impl GameState +{ + pub fn new() + -> Self { + let timeman = TimeMan::new(0); + let sol_system = match SolarSystem::new_from_known_star("sol") + { + Ok(system) => system, + Err(e) => panic!("Unable to create sol system : {}", e) + }; + + Self { + timeman: timeman, + solar_systems: vec![ sol_system ] + } + } + + pub fn solar_systems(&self) -> &[SolarSystem] + { self.solar_systems.as_slice() } + + pub fn timeman(&self) -> &TimeMan + { &self.timeman } + + pub fn timeman_mut(&mut self) -> &mut TimeMan + { &mut self.timeman } +} + +impl ApplicationHandler for SystemicApp +{ + fn resumed( + &mut self, + event_loop: &ActiveEventLoop + ) { + match self.window { + Some(_) => {} + None => self.create_window(event_loop) + } + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: winit::event::WindowEvent, + ) { + let window = match &mut self.window { + Some(w) => w, + None => return + }; + + let game_state = match &self.game_state { + Some(state) => state, + None => return + }; + + window.on_event(&event); + + match event { + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::Resized(size) => { + window.resize(size.width, size.height); + } + WindowEvent::RedrawRequested => { + let now = Instant::now(); + let delta_time = now - self.last_render_time; + self.last_render_time = now; + + game_state.borrow_mut().timeman.update(); + { + window.update(game_state, delta_time); + } + match window.render(game_state) { + Ok(_) => {} + Err(wgpu::SurfaceError::Outdated | wgpu::SurfaceError::Lost) => { + let size = window.size(); + window.resize(size.width, size.height); + }, + Err(e) => { + log::error!("Unable to render: {}", e); + } + } + if delta_time < TARGET_DT { + thread::sleep(TARGET_DT - delta_time); + } + } + WindowEvent::KeyboardInput { + event: KeyEvent { + physical_key: PhysicalKey::Code(key_code), + state: key_state, + .. + }, + .. + } => { + window.keyboard_input(game_state, key_code, key_state); + } + _ => {} + } + } +} // impl ApplicationHandler for SystemicApp + +fn main() + -> Result<(), EventLoopError> +{ + env_logger::init(); + let event_loop = EventLoop::new().unwrap(); + + event_loop.set_control_flow(ControlFlow::Poll); + + let mut app = SystemicApp { + window: None, + wgpu_instance: wgpu::Instance::default(), + last_render_time: Instant::now(), + game_state: None + }; + + event_loop.run_app(&mut app) +} diff --git a/src/solar_system.rs b/src/solar_system.rs new file mode 100644 index 0000000..e8137f0 --- /dev/null +++ b/src/solar_system.rs @@ -0,0 +1,161 @@ +use serde::{Deserialize}; +use crate::{known_stars::{KNOWN_STARS, StarNotFoundError}, timeman::Second}; +use std::error::Error; + +const GRAVITATIONAL_CONSTANT: f64 = 6.67408e-11; + +pub type Kilograms = f64; +pub type Kilometers = f64; +pub type Percentage = f64; +pub type Angle = f64; + +pub type BodyId = usize; +pub type SystemId = usize; + +#[derive(Debug, Deserialize)] +pub struct SerialOrbitalBody +{ + name: String, + orbits: BodyId, + mass: Kilograms, + radius: Kilometers, + + eccentricity: Percentage, + inclination: Angle, + long_asc_node: Angle, + long_periapsis: Angle, + sgp: f64, + mean_long: Angle, + + semi_major_axis: Kilometers, +} + +pub struct OrbitalBody +{ + body: SerialOrbitalBody, + position: Option<(Second, cgmath::Point3)> +} + +pub struct SolarSystem +{ + name: String, + bodies: Vec, +} + +impl SolarSystem +{ + pub fn new_from_csv( + data: &'static str) + -> Result> { + let data_reader = stringreader::StringReader::new(data); + let mut body_reader = csv::Reader::from_reader(data_reader); + + let mut bodies = Vec::::new(); + + for result in body_reader.deserialize() { + let record: SerialOrbitalBody = result?; + + println!("New body: {:?}", record); + + bodies.push(OrbitalBody { body: record, position: None }); + } + + Ok(Self { + name: bodies[0].name().clone(), + bodies: bodies, + }) + } + + pub fn new_from_known_star( + star: &'static str) + -> Result> { + let star_csv = match KNOWN_STARS.get(star).copied() { + Some(csv) => csv, + None => return Err(Box::new(StarNotFoundError { star: star })) + }; + SolarSystem::new_from_csv(star_csv) + } + + pub fn name(&self) -> &String { &self.name } + + pub fn bodies(&self) + -> &[OrbitalBody] + { + self.bodies.as_slice() + } +} + +impl OrbitalBody +{ + pub fn name(&self) -> &String { &self.body.name } + pub fn radius(&self) -> f32 { self.body.radius as f32 } + + pub fn position(&self, time: Second) + -> Option> + { + match self.position { + Some((cache_time, pos)) => { + if time == cache_time { + return Some(pos); + } + return None; + }, + None => None + } + } + + fn calculate_orbit( + &self, + time: i64) + -> cgmath::Point3 { + cgmath::Point3 { x: 0.0, y: 0.0, z: 0.0 } + /*let arg_periapsis = self.long_periapsis - self.long_asc_node; + + + let mean_angular_motion: f64 = ( + self.sgp as f64 / (self.semi_major_axis as f64).powf(3.0) + ).sqrt(); + + let mean_anomaly = (self.mean_long - self.long_periapsis) + (mean_angular_motion * time as f64) as f32; + + let mut eccentric_anomaly = mean_anomaly + self.eccentricity * mean_anomaly.sin(); + for _ in 0..100 { + let new_eccentric = eccentric_anomaly + + (mean_anomaly - eccentric_anomaly + self.eccentricity * eccentric_anomaly.sin()) / + (1.0 - self.eccentricity * eccentric_anomaly.cos()); + if (new_eccentric - eccentric_anomaly).abs() < 1e-6 { + eccentric_anomaly = new_eccentric; + break; + } + eccentric_anomaly = new_eccentric; + } + + let beta = self.eccentricity / 1.0 + (1.0 - self.eccentricity * self.eccentricity).sqrt(); + let true_anomaly = eccentric_anomaly + 2.0 * + ((beta * eccentric_anomaly.sin()) / (1.0 - beta * eccentric_anomaly.cos())).atan(); + + let radius: f64 = self.semi_major_axis * (1.0 - self.eccentricity * eccentric_anomaly.cos()) as f64; + + let ohm_sin = self.long_asc_node.sin(); + let ohm_cos = self.long_asc_node.cos(); + + let inc_sin = self.inclination.sin(); + let inc_cos = self.inclination.cos(); + + let per_anom_sin = (arg_periapsis + true_anomaly).sin(); + let per_anom_cos = (arg_periapsis + true_anomaly).cos(); + + let x: f32 = (radius as f32) * ( + (ohm_cos * per_anom_cos) - + (ohm_sin * per_anom_sin * inc_cos)); + let y: f32 = (radius as f32) * ( + (ohm_sin * per_anom_cos) + + (ohm_cos * per_anom_sin * inc_cos)); + let z: f32 = (radius as f32) * (inc_sin * per_anom_sin); + + OrbitState { + position: cgmath::Vector3::new(x, y, z) + }*/ + } +} + diff --git a/src/tacmap.rs b/src/tacmap.rs new file mode 100644 index 0000000..89d21aa --- /dev/null +++ b/src/tacmap.rs @@ -0,0 +1,158 @@ +pub mod camera; +pub mod render; + +mod tacmap { + pub use super::render; + pub use super::camera; +} + +use std::cell::RefCell; +use std::time::Duration; + +use winit::event::ElementState; +use winit::keyboard::KeyCode; + +use crate::GameState; +use crate::canvas::Canvas; +use crate::solar_system::SolarSystem; +use crate::solar_system::SystemId; +use crate::wgpuctx::WgpuCtx; +use render::*; +use camera::*; +use crate::window::ui::GameWindowUiState; + +pub struct TacticalMap +{ + canvas: Canvas, + camera: Camera, + pmatrix: Projection, + + camera_controller: CameraController, + renderstate: Option<(SystemId, BodyRenderer)> +} + +impl TacticalMap +{ + pub fn new( + wgpuctx: &WgpuCtx, + position: winit::dpi::LogicalPosition, + size: winit::dpi::LogicalSize) + -> Self { + let surface_size = winit::dpi::PhysicalSize::new( + wgpuctx.surface_config().width, + wgpuctx.surface_config().height + ); + + let canvas = Canvas::new( + wgpuctx, + surface_size, + position, + size).unwrap(); + + let camera = Camera::new( + wgpuctx, + cgmath::Point3::::new(0.0, 0.0, 1.0), + cgmath::Deg(-90.0), + cgmath::Rad(0.0)); + + let projection = Projection::new( + surface_size.width, + surface_size.height, + cgmath::Deg(60.0), + (0.1, 1000.0)); + + Self { + canvas: canvas, + camera: camera, + pmatrix: projection, + camera_controller: CameraController::new(), + renderstate: None, + } + } + + pub fn resize( + &mut self, + wgpuctx: &WgpuCtx, + width: u32, + height: u32) + { + self.canvas.resize(wgpuctx, width, height); + self.pmatrix.resize(width, height); + } + + pub fn update( + &mut self, + game_state: &RefCell, + ui_state: &mut GameWindowUiState, + dt: Duration) + { + self.camera_controller.update(&mut self.camera, dt); + ui_state.camera_scale = self.camera.get_scale(); + } + + pub fn keyboard_input( + &mut self, + game_state: &RefCell, + key_code: KeyCode, + key_state: ElementState) + { + self.camera_controller.keyboard_input(key_code, key_state); + } + + pub fn draw( + &mut self, + wgpuctx: &WgpuCtx, + game_state: &RefCell, + current_system: Option) + -> Result<(), wgpu::SurfaceError> + { + let game_state = game_state.borrow(); + + let current_system_id = match current_system { + Some(system) => system, + None => return Ok(()) + }; + let solar_systems = game_state.solar_systems(); + let current_system = &solar_systems[current_system_id]; + + self.camera.update_buffer(wgpuctx, &self.pmatrix); + + let tacrender = match &mut self.renderstate { + Some((id, render)) => { + if *id != current_system_id { + *id = current_system_id; + render.mark_to_rebuild(); + } + render + }, + None => { + let tmp = render::BodyRenderer::new(wgpuctx); + self.renderstate = Some((current_system_id, tmp)); + &mut self.renderstate.as_mut().unwrap().1 + } + }; + + //Update buffers for the current system and time. + tacrender.rebuild(wgpuctx, current_system); + match tacrender.update( + wgpuctx, current_system, + game_state.timeman().seconds()) + { + Ok(()) => {}, + Err(e) => println!("Tactical map render update error: {}", e) + } + + tacrender.render(wgpuctx, &self.canvas, &self.camera); + + Ok(()) + } + + pub fn present( + &mut self, + screen_pass: &mut wgpu::RenderPass) + -> Result<(), wgpu::SurfaceError> + { + self.canvas.present(screen_pass)?; + Ok(()) + } +} // impl SystemViewport diff --git a/src/tacmap/camera.rs b/src/tacmap/camera.rs new file mode 100644 index 0000000..1a2787b --- /dev/null +++ b/src/tacmap/camera.rs @@ -0,0 +1,272 @@ +use std::time::Duration; + +use cgmath::{InnerSpace, Point3, Rad, Vector2, Vector3, Vector4, Zero, perspective}; +use winit::{event::ElementState, keyboard::KeyCode}; + +use crate::{solar_system::BodyId, wgpuctx::WgpuCtx}; + +pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4 = cgmath::Matrix4::from_cols( + Vector4::new(1.0, 0.0, 0.0, 0.0), + Vector4::new(0.0, 1.0, 0.0, 0.0), + Vector4::new(0.0, 0.0, 0.5, 0.0), + Vector4::new(0.0, 0.0, 0.5, 1.0) +); + +pub struct Camera +{ + position: Point3, + pitch: Rad, + yaw: Rad, + scale: f32, + + target: Option, + + buffer: wgpu::Buffer, + staging_buffer: wgpu::Buffer, + bindgroup: wgpu::BindGroup +} + +pub struct CameraController +{ + position_pos_delta: Vector3, + position_neg_delta: Vector3, + rotation_pos_delta: Vector2, + rotation_neg_delta: Vector2, + scale_delta: Vector2, +} + +#[repr(C)] +#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct CameraUniform +{ + view: [[f32;4];4], + proj: [[f32;4];4], + scale: f32 +} + +pub struct Projection +{ + aspect: f32, + fovy: Rad, + clip: (f32, f32) +} + +impl Camera +{ + pub fn new< + V: Into>, + Y: Into>, + P: Into> + >( + wgpuctx: &WgpuCtx, + position: V, + yaw: Y, + pitch: P) + -> Self { + use wgpu::BufferUsages; + let buffer = wgpuctx.create_buffer( + &wgpu::BufferDescriptor { + label: Some("Camera buffer"), + size: 144, + usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST, + mapped_at_creation: false + } + ); + + let staging_buffer = wgpuctx.create_buffer( + &wgpu::wgt::BufferDescriptor { + label: Some("Camera staging buffer"), + size: 144, + usage: BufferUsages::COPY_SRC | BufferUsages::COPY_DST, + mapped_at_creation: false + } + ); + + let bind_group_layout = Camera::bindgroup_layout(wgpuctx); + let bind_group = wgpuctx.device().create_bind_group( + &wgpu::BindGroupDescriptor { + label: Some("Camera bind group"), + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding() + } + ] + } + ); + Self { + position: position.into(), + yaw: yaw.into(), + pitch: pitch.into(), + scale: 1.0, + target: None, + buffer: buffer, + staging_buffer: staging_buffer, + bindgroup: bind_group + } + } + + pub fn get_scale(&self) -> f32 + { self.scale } + + pub fn bindgroup_layout(wgpuctx: &WgpuCtx) + -> wgpu::BindGroupLayout + { + wgpuctx.device().create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + label: Some("Camera bind group layout"), + 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 + } + ] + } + ) + } + + pub fn bindgroup(&self) + -> &wgpu::BindGroup + { &self.bindgroup } + + pub fn stage_changes( + &self, + encoder: &mut wgpu::CommandEncoder) + { + let buffer_size = size_of::(); + encoder.copy_buffer_to_buffer(&self.staging_buffer, 0, &self.buffer, 0, Some(buffer_size as u64)); + } + + pub fn update_buffer( + &self, + wgpuctx: &WgpuCtx, + projection: &Projection) + { + wgpuctx.queue().write_buffer(&self.staging_buffer, 0, bytemuck::cast_slice(&[self.uniform(projection)])); + } + + pub fn view_matrix( + &self) + -> cgmath::Matrix4 { + + let (yaw_sin, yaw_cos) = self.yaw.0.sin_cos(); + let (pitch_sin, pitch_cos) = self.pitch.0.sin_cos(); + + cgmath::Matrix4::look_to_rh( + self.position, + Vector3::new( + pitch_cos * yaw_cos, + pitch_sin, + pitch_cos * yaw_sin + ).normalize(), + Vector3::unit_y() + ) + } + + pub fn uniform( + &self, + projection: &Projection) + -> CameraUniform + { + CameraUniform { + view: (self.view_matrix()).into(), + proj: (OPENGL_TO_WGPU_MATRIX * projection.projection_matrix()).into(), + scale: self.scale + } + } +} //impl Camera + +impl CameraController +{ + pub fn new() -> Self { + Self { + position_pos_delta: Vector3::new(0.0, 0.0, 0.0), + position_neg_delta: Vector3::new(0.0, 0.0, 0.0), + rotation_pos_delta: Vector2::new(0.0, 0.0), + rotation_neg_delta: Vector2::new(0.0, 0.0), + scale_delta: Vector2::new(0.0, 0.0), + } + } + + pub fn keyboard_input( + &mut self, + key_code: KeyCode, + key_state: ElementState) + { + let press_q = if key_state == ElementState::Pressed { 1.0 } else { 0.0 }; + match key_code { + KeyCode::KeyW => { self.position_pos_delta.z = press_q; }, + KeyCode::KeyS => { self.position_neg_delta.z = press_q; }, + KeyCode::KeyA => { self.position_neg_delta.x = press_q; }, + KeyCode::KeyD => { self.position_pos_delta.x = press_q; }, + KeyCode::Space => { self.position_pos_delta.y = press_q; }, + KeyCode::ShiftLeft => { self.position_neg_delta.y = press_q; }, + + KeyCode::KeyQ => { self.scale_delta.x = press_q; }, + KeyCode::KeyE => { self.scale_delta.y = press_q; }, + _ => {} + } + } + + pub fn update( + &mut self, + camera: &mut Camera, + dt: Duration) + { + let dt = dt.as_secs_f32(); + let speed = 1.0; + + camera.pitch.0 += (self.rotation_pos_delta.x - self.rotation_neg_delta.x) * speed * dt; + camera.yaw.0 += (self.rotation_pos_delta.y - self.rotation_neg_delta.y) * speed * dt; + + let (yaw_sin, yaw_cos) = camera.yaw.0.sin_cos(); + let forward = Vector3::new(yaw_cos, 0.0, yaw_sin).normalize(); + let right = Vector3::new(-yaw_sin, 0.0, yaw_cos).normalize(); + let up = Vector3::new(0.0, 1.0, 0.0); + + camera.position += forward * (self.position_pos_delta.z - self.position_neg_delta.z) * speed * dt; + camera.position += right * (self.position_pos_delta.x - self.position_neg_delta.x) * speed * dt; + camera.position += up * (self.position_pos_delta.y - self.position_neg_delta.y) * speed * dt; + + camera.scale *= 1.0 + ((self.scale_delta.y - self.scale_delta.x) * 0.1); + camera.scale = f32::max(1e-16, f32::min(1.0, camera.scale)); + } +} + +impl Projection { + pub fn new< + F: Into> + >( + width: u32, + height: u32, + fovy: F, + clip: (f32, f32)) + -> Self { + Self { + aspect: width as f32 / height as f32, + fovy: fovy.into(), + clip: clip + } + } + + pub fn resize( + &mut self, + width: u32, + height: u32) + { + self.aspect = width as f32 / height as f32; + } + + pub fn projection_matrix( + &self) + -> cgmath::Matrix4 { + perspective(self.fovy, self.aspect, self.clip.0, self.clip.1) + } +} diff --git a/src/tacmap/render.rs b/src/tacmap/render.rs new file mode 100644 index 0000000..c22878e --- /dev/null +++ b/src/tacmap/render.rs @@ -0,0 +1,233 @@ +use std::{fmt::Display, num::NonZero}; +use std::error::Error; + +use crate::canvas::Canvas; +use crate::solar_system::Kilometers; +use crate::tacmap::camera::Camera; +use crate::wgpuctx::RenderPassBuilder; +use crate::{solar_system::{SolarSystem, SystemId}, timeman::Second, vertex::{self, Vertex}, wgpuctx::{WgpuCtx, pipeline::RenderPipelineBuilder}}; + +struct BodyInstance +{ + position: cgmath::Point3, + radius: f32 +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +struct BodyInstanceRaw +{ + position: [f32;3], + radius: f32 +} + +#[derive(Debug, Clone)] +pub struct NeedsRebuildError; + +impl Display for NeedsRebuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "RenderState needs to be rebuilt before updating!") + } +} + +impl Error for NeedsRebuildError {} + +pub struct BodyRenderer +{ + needs_rebuild: bool, + last_time: Option, + + pipeline: wgpu::RenderPipeline, + + body_vertex_buffer: wgpu::Buffer, + body_instance_buffer: Option<(usize, wgpu::Buffer)> +} + +impl BodyRenderer +{ + pub fn new( + wgpuctx: &WgpuCtx) + -> Self { + let quad_vertices = vertex::QUAD_VERTICES; + + let body_vertex_buffer = wgpuctx.create_buffer_init( + &wgpu::util::BufferInitDescriptor { + label: None, + contents: bytemuck::cast_slice(quad_vertices), + usage: wgpu::BufferUsages::VERTEX + } + ); + + let shader = wgpuctx.create_shader( + wgpu::include_wgsl!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/assets/shaders/tacbody.wgsl") + )); + + let render_pipeline = RenderPipelineBuilder::new(&shader) + .add_bindgroup(&Camera::bindgroup_layout(wgpuctx)) + .add_vertex_layout(Vertex::descr()) + .add_vertex_layout(BodyInstanceRaw::descr()) + .build(Some("Tactical map render pipeline"), wgpuctx); + + Self { + needs_rebuild: true, + last_time: None, + pipeline: render_pipeline, + body_vertex_buffer, + body_instance_buffer: None + } + } + + pub fn mark_to_rebuild(&mut self) + { self.needs_rebuild = true; } + + pub fn rebuild( + &mut self, + wgpuctx: &WgpuCtx, + solar_system: &SolarSystem) + { + if self.body_instance_buffer.is_some() && !self.needs_rebuild { + return; + } + + match self.body_instance_buffer.as_mut() { + Some(buffer) => { + buffer.1.destroy(); + self.body_instance_buffer = None; + } + None => {} + } + + let bodies = solar_system.bodies(); + let buffer_len = bodies.len() * size_of::(); + + let buffer = wgpuctx.create_buffer( + &wgpu::BufferDescriptor { + label: Some("Tactical map bodies instance buffer"), + size: buffer_len as u64, + usage: wgpu::BufferUsages::COPY_DST | + wgpu::BufferUsages::VERTEX, + mapped_at_creation: false + } + ); + + self.last_time = None; + self.body_instance_buffer = Some((bodies.len(), buffer)); + } + + pub fn update( + &mut self, + wgpuctx: &WgpuCtx, + solar_system: &SolarSystem, + time: Second) + -> Result<(), Box> + { + //If the last updated time is the same, we don't need to update + //the positions of all the bodies. + match self.last_time { + Some(last_time) => { + if last_time == time { + return Ok(()); + } + } + None => {} + } + + self.last_time = Some(time); + let (_, bodies_buffer) = match &self.body_instance_buffer { + Some(tuple) => tuple, + None => return Err(Box::new(NeedsRebuildError)) + }; + + let bodies = solar_system.bodies(); + let body_instances = bodies.iter().map(|body| { + let position = match body.position(time) { + Some(pos) => pos, + None => return BodyInstanceRaw { + position: [0.0, 0.0, 0.0], + radius: body.radius() } + }; + BodyInstance { + position: position, + radius: body.radius() + }.raw() + }).collect::>(); + + wgpuctx.queue().write_buffer(bodies_buffer, 0, bytemuck::cast_slice(&body_instances)); + //Update the canvas texture. + + Ok(()) + } + + pub fn render( + &self, + wgpuctx: &WgpuCtx, + canvas: &Canvas, + camera: &Camera) + { + let (num_bodies, bodies_buffer) = match &self.body_instance_buffer { + Some(tuple) => tuple, + None => return + }; + + let mut encoder = wgpuctx.create_default_encoder("Tactical map renderer encoder"); + let view = canvas.view(); + + { + camera.stage_changes(&mut encoder); + } + { + let mut pass = RenderPassBuilder::new("Tactiacal map render pass", view) + .clear_color(wgpu::Color { r: 0.0, g: 0.0, b: 0.1, a: 1.0 }) + .build(&mut encoder); + + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, camera.bindgroup(), &[]); + + pass.set_vertex_buffer(0, self.body_vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, bodies_buffer.slice(..)); + + pass.draw(0..6, 0..num_bodies.clone() as _); + } + + wgpuctx.submit_encoder(encoder); + } +} // impl RenderState + +impl BodyInstance +{ + fn raw(&self) -> BodyInstanceRaw + { + BodyInstanceRaw { + position: [ + self.position.x as f32, + self.position.y as f32, + self.position.z as f32 ], + radius: self.radius + } + } +} // impl BodyInstance + +impl BodyInstanceRaw +{ + fn descr() -> wgpu::VertexBufferLayout<'static> { + use std::mem; + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 2, + format: wgpu::VertexFormat::Float32x3 + }, + wgpu::VertexAttribute { + offset: mem::size_of::<[f32;3]>() as wgpu::BufferAddress, + shader_location: 3, + format: wgpu::VertexFormat::Float32 + } + ] + } + } +} diff --git a/src/texture.rs b/src/texture.rs new file mode 100644 index 0000000..be07dd3 --- /dev/null +++ b/src/texture.rs @@ -0,0 +1,96 @@ +use crate::wgpuctx::WgpuCtx; + +pub struct Texture +{ + texture: wgpu::Texture, + view: wgpu::TextureView, + sampler: wgpu::Sampler, + pub bindgrouplayout: wgpu::BindGroupLayout, + bindgroup: wgpu::BindGroup, +} + +impl Texture +{ + pub fn new( + device: &wgpu::Device, + descr: wgpu::TextureDescriptor) + -> Self + { + let texture = device.create_texture(&descr); + let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); + let sampler = device.create_sampler( + &wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::Repeat, + address_mode_v: wgpu::AddressMode::Repeat, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + ..Default::default() + } + ); + + let bind_group_layout = device.create_bind_group_layout( + &wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { + filterable: true + }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false + }, + count: None + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None + } + ] + } + ); + + let bind_group = device.create_bind_group( + &wgpu::BindGroupDescriptor { + label: None, + layout: &bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&view) + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler) + } + ] + } + ); + + Self { + texture: texture, + view: view, + sampler: sampler, + bindgrouplayout: bind_group_layout, + bindgroup: bind_group, + } + } + + pub fn view(&self) + -> &wgpu::TextureView + { &self.view } + + pub fn use_on_pass( + &self, + pass: &mut wgpu::RenderPass, + index: u32) + { + pass.set_bind_group(index, &self.bindgroup, &[]); + } +} // impl Texture diff --git a/src/timeman.rs b/src/timeman.rs new file mode 100644 index 0000000..9d17999 --- /dev/null +++ b/src/timeman.rs @@ -0,0 +1,69 @@ +use std::cell::RefCell; +use std::time::Duration; +use std::{fmt::Display, string}; +use std::error::Error; + +use crate::GameState; +use crate::window::ui::GameWindowUiState; + +pub type Second = u64; + +pub struct TimeMan +{ + time: Second, + auto_tick: Option +} + +impl TimeMan +{ + pub fn new(time: Second) + -> Self { + Self { + time, + auto_tick: None + } + } + + pub fn seconds(&self) + -> Second { + self.time + } + + pub fn advance( + &mut self, + by: Second) + { + self.time += by; + } + + pub fn update( + &mut self) + { + match self.auto_tick { + Some(advance) => { self.time += advance; }, + None => {} + } + } + + pub fn format_duration(time: Second) -> String + { + let seconds = time % 60; + let minutes = (time / 60) % 60; + let hours = (time / (60 * 60)) % 24; + let days = (time / (60 * 60 * 24)) % 365; + let years = time / (60 * 60 * 24 * 365); + + if time == 0 { + return "0s".to_string(); + } + + format!("{}{}{}{}{}", + if seconds > 0 { format!("{}s", seconds) } else { format!("") }, + if minutes > 0 { format!("{}m", minutes) } else { format!("") }, + if hours > 0 { format!("{}h", hours) } else { format!("") }, + if days > 0 { format!("{}d", days) } else { format!("") }, + if years > 0 { format!("{}y", years) } else { format!("") }) + } + + +} //impl TimeMan diff --git a/src/vertex.rs b/src/vertex.rs new file mode 100644 index 0000000..aab7d02 --- /dev/null +++ b/src/vertex.rs @@ -0,0 +1,33 @@ +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex +{ + pub position: [f32;3], + pub uv: [f32;2] +} + +impl Vertex +{ + const ATTRIBS: [wgpu::VertexAttribute;2] = + wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2]; + + pub fn descr() + -> wgpu::VertexBufferLayout<'static> { + use std::mem; + + wgpu::VertexBufferLayout { + array_stride: mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS + } + } +} + +pub const QUAD_VERTICES: &[Vertex] = &[ + Vertex { position: [ -1.0, -1.0, 0.0 ], uv: [ 0.0, 1.0 ] }, + Vertex { position: [ 1.0, -1.0, 0.0 ], uv: [ 1.0, 1.0 ] }, + Vertex { position: [ 1.0, 1.0, 0.0 ], uv: [ 1.0, 0.0 ] }, + Vertex { position: [ 1.0, 1.0, 0.0 ], uv: [ 1.0, 0.0 ] }, + Vertex { position: [ -1.0, 1.0, 0.0 ], uv: [ 0.0, 0.0 ] }, + Vertex { position: [ -1.0, -1.0, 0.0 ], uv: [ 0.0, 1.0 ] } +]; diff --git a/src/wgpuctx/mod.rs b/src/wgpuctx/mod.rs new file mode 100644 index 0000000..f8aa3ec --- /dev/null +++ b/src/wgpuctx/mod.rs @@ -0,0 +1,245 @@ +use std::sync::Arc; +use std::error::Error; +use wgpu::util::DeviceExt; +use winit::window::{Window}; + +use crate::texture::Texture; + +pub mod pipeline; + +pub struct WgpuCtx +{ + surface: wgpu::Surface<'static>, + surface_conf: wgpu::SurfaceConfiguration, + surface_texture: Option, + is_configured: bool, + + adapter: wgpu::Adapter, + device: wgpu::Device, + queue: wgpu::Queue, +} + +pub struct RenderPassBuilder<'encoder> +{ + label: &'static str, + view: &'encoder wgpu::TextureView, + clear_color: wgpu::Color +} + +impl WgpuCtx +{ + pub async fn new( + instance: &wgpu::Instance, + window: Arc, + ) -> Self { + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + compatible_surface: Some(&surface), + ..Default::default() + }).await.expect("Failed to find valid adapter"); + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: None, + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::downlevel_defaults(), + experimental_features: wgpu::ExperimentalFeatures::disabled(), + memory_hints: wgpu::MemoryHints::Performance, + trace: wgpu::Trace::Off + }).await.expect("Failed to find device and queue"); + + let size = window.inner_size(); + let surface_config = surface.get_default_config(&adapter, size.width, size.height).unwrap(); + + Self { + surface: surface, + surface_conf: surface_config, + surface_texture: None, + is_configured: false, + adapter: adapter, + device: device, + queue: queue + } + } + + pub fn resize( + &mut self, + width: u32, + height: u32) + { + self.surface_conf.width = width; + self.surface_conf.height = height; + self.surface.configure(&self.device, &self.surface_conf); + self.is_configured = true; + } + + pub fn is_ready( + &self) + -> bool { + self.is_configured + } + + pub fn surface_config( + &self) + -> wgpu::SurfaceConfiguration { + self.surface_conf.clone() + } + + pub fn adapter( + &self) + -> &wgpu::Adapter { + &self.adapter + } + + pub fn device( + &self) + -> &wgpu::Device { + &self.device + } + + pub fn queue( + &self) + -> &wgpu::Queue { + &self.queue + } + + pub fn prepare_surface( + &mut self, + view_descr: &wgpu::TextureViewDescriptor) + -> Result + { + let texture = self.surface.get_current_texture()?; + let view = texture.texture.create_view(view_descr); + + self.surface_texture = Some(texture); + Ok(view) + } + + pub fn create_encoder( + &self, + descr: &wgpu::CommandEncoderDescriptor) + -> wgpu::CommandEncoder + { + self.device.create_command_encoder(descr) + } + + pub fn create_default_encoder( + &self, + label: &'static str) + -> wgpu::CommandEncoder { + self.create_encoder( + &wgpu::CommandEncoderDescriptor { + label: Some(label) + } + ) + } + + pub fn submit_encoder( + &self, + encoder: wgpu::CommandEncoder) + { + self.queue.submit(std::iter::once(encoder.finish())); + } + + pub fn present_surface( + &mut self) + { + let texture = self.surface_texture.take(); + match texture { + Some(t) => t.present(), + None => {} + } + } + + pub fn create_shader( + &self, + module_descr: wgpu::ShaderModuleDescriptor) + -> wgpu::ShaderModule { + self.device.create_shader_module(module_descr) + } + + pub fn create_pipeline_layout( + &self, + layout_descr: &wgpu::PipelineLayoutDescriptor) + -> wgpu::PipelineLayout { + self.device.create_pipeline_layout(layout_descr) + } + + pub fn create_render_pipeline( + &self, + pipeline_layout: &wgpu::RenderPipelineDescriptor) + -> wgpu::RenderPipeline { + self.device.create_render_pipeline(pipeline_layout) + } + + pub fn create_buffer_init( + &self, + descr: &wgpu::util::BufferInitDescriptor + ) -> wgpu::Buffer { + self.device.create_buffer_init(descr) + } + + pub fn create_buffer( + &self, + descr: &wgpu::BufferDescriptor) + -> wgpu::Buffer { + self.device.create_buffer(descr) + } + + pub fn new_texture( + &self, + descr: wgpu::TextureDescriptor) + -> Texture + { + Texture::new(&self.device, descr) + } + +} //impl WgpuCtx + +impl<'encoder> RenderPassBuilder<'encoder> +{ + pub fn new( + label: &'static str, + view: &'encoder wgpu::TextureView) + -> Self { + Self { + label: label, + view: view, + clear_color: wgpu::Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 } + } + } + + pub fn clear_color( + mut self, + color: wgpu::Color) + -> Self { + self.clear_color = color; + self + } + + pub fn build( + self, + encoder: &'encoder mut wgpu::CommandEncoder) + -> wgpu::RenderPass<'encoder> + { + encoder.begin_render_pass( + &wgpu::RenderPassDescriptor { + label: Some(self.label), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: self.view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(self.clear_color), + store: wgpu::StoreOp::Store + } + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + } + ) + } +} diff --git a/src/wgpuctx/pipeline.rs b/src/wgpuctx/pipeline.rs new file mode 100644 index 0000000..7606203 --- /dev/null +++ b/src/wgpuctx/pipeline.rs @@ -0,0 +1,121 @@ + +use crate::wgpuctx::WgpuCtx; + +pub struct RenderPipelineBuilder<'a> +{ + bind_groups: Vec<&'a wgpu::BindGroupLayout>, + shader: &'a wgpu::ShaderModule, + + vertex_entry_point: Option<&'static str>, + fragment_entry_point: Option<&'static str>, + + vertex_comp_options: Option>, + fragment_comp_options: Option>, + + vertex_buffer_layouts: Vec> +} + +impl<'a> RenderPipelineBuilder<'a> +{ + pub fn new( + shader: &'a wgpu::ShaderModule) + -> Self { + Self { + bind_groups: Vec::new(), + shader: shader, + + vertex_entry_point: Some("vs_main"), + fragment_entry_point: Some("fs_main"), + + vertex_comp_options: None, + fragment_comp_options: None, + + vertex_buffer_layouts: Vec::new() + } + } + + pub fn add_bindgroup( + mut self, + bindgroup: &'a wgpu::BindGroupLayout) + -> Self { + self.bind_groups.push(bindgroup); + self + } + + pub fn add_vertex_layout( + mut self, + layout: wgpu::VertexBufferLayout<'a>) + -> Self { + self.vertex_buffer_layouts.push(layout); + self + } + + pub fn build( + self, + label: Option<&'static str>, + wgpuctx: &WgpuCtx) + -> wgpu::RenderPipeline + { + let layout_descr = wgpu::PipelineLayoutDescriptor { + label: label, + bind_group_layouts: self.bind_groups.as_slice(), + push_constant_ranges: &[] + }; + + let layout = wgpuctx.create_pipeline_layout(&layout_descr); + + wgpuctx.create_render_pipeline( + &wgpu::RenderPipelineDescriptor { + label: label, + layout: Some(&layout), + vertex: wgpu::VertexState { + module: self.shader, + entry_point: self.vertex_entry_point, + compilation_options: match self.vertex_comp_options { + Some(option) => option, + None => wgpu::PipelineCompilationOptions::default() + }, + buffers: self.vertex_buffer_layouts.as_slice() + }, + fragment: Some(wgpu::FragmentState { + module: self.shader, + entry_point: self.fragment_entry_point, + compilation_options: match self.fragment_comp_options { + Some(option) => option, + None => wgpu::PipelineCompilationOptions::default() + }, + targets: &[Some(wgpu::ColorTargetState { + format: wgpuctx.surface_config().format, + blend: Some(wgpu::BlendState{ + color: wgpu::BlendComponent { + src_factor: wgpu::BlendFactor::SrcAlpha, + dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha, + operation: wgpu::BlendOperation::Add + }, + alpha: wgpu::BlendComponent::OVER + }), + 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: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false + }, + multiview: None, + cache: None + + } + ) + } +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..0be88e7 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,148 @@ +pub mod ui; + +mod window { + pub use super::ui; +} + +use std::cell::RefCell; +use std::sync::{Arc}; +use std::time::Duration; + +use winit::event::{ElementState, WindowEvent}; +use winit::keyboard::KeyCode; + +use crate::tacmap::TacticalMap; +use crate::{GameState, SystemicApp}; +use crate::solar_system::{SolarSystem, SystemId}; +use crate::wgpuctx::{RenderPassBuilder, WgpuCtx}; +use crate::eguictx::EguiCtx; + +use ui::*; + +pub struct GameWindow +{ + window: Arc, + wgpuctx: WgpuCtx, + eguictx: EguiCtx, + + tactical_map: TacticalMap, + + ui_state: GameWindowUiState +} + +impl GameWindow +{ + pub fn new( + instance: &wgpu::Instance, + event_loop: &winit::event_loop::ActiveEventLoop) + -> Result + { + let window_attrs = winit::window::Window::default_attributes() + .with_title("Systemic 4X") + .with_inner_size(winit::dpi::LogicalSize::new(640, 480)); + + let window = Arc::new(event_loop.create_window(window_attrs).unwrap()); + + let wgpuctx = pollster::block_on(WgpuCtx::new(instance, window.clone())); + let eguictx = EguiCtx::new(&window, &wgpuctx); + + let tacmap = TacticalMap::new( + &wgpuctx, + winit::dpi::LogicalPosition::new(0.0, 0.0), + winit::dpi::LogicalSize::new(1.0, 1.0)); + + Ok(Self { + window: window, + wgpuctx: wgpuctx, + eguictx: eguictx, + tactical_map: tacmap, + + ui_state: Default::default() + }) + } + + pub fn update( + &mut self, + game_state: &RefCell, + dt: Duration) + { + self.tactical_map.update(game_state, &mut self.ui_state, dt); + } + + pub fn keyboard_input( + &mut self, + game_state: &RefCell, + key_code: KeyCode, + key_state: ElementState) + { + self.tactical_map.keyboard_input(game_state, key_code, key_state); + } + + pub fn render( + &mut self, + game_state: &RefCell) + -> Result<(), wgpu::SurfaceError> { + if !self.wgpuctx.is_ready() { + return Ok(()); + } + + self.tactical_map.draw( + &self.wgpuctx, + game_state, + self.ui_state.current_system)?; + + let mut encoder = self.wgpuctx.create_default_encoder("Systemic window command encoder"); + let view = self.wgpuctx.prepare_surface(&wgpu::TextureViewDescriptor::default())?; + { + let mut pass = RenderPassBuilder::new("Systemic window render pass", &view) + .clear_color(wgpu::Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }) + .build(&mut encoder); + + //Draw the tactical map canvas. + self.tactical_map.present(&mut pass)?; + } + { + self.eguictx.prepare(&self.window); + self.ui_state = GameWindowUiState::render(&self.ui_state, game_state, &self.eguictx); + + self.eguictx.present( + &self.window, + &self.wgpuctx, + &mut encoder, + &view); + } + + self.wgpuctx.submit_encoder(encoder); + self.wgpuctx.present_surface(); + self.window.request_redraw(); + + Ok(()) + } + + pub fn on_event( + &mut self, + event: &WindowEvent) + { + if self.eguictx.window_event(&self.window, event).consumed { + return; + } + } + + pub fn resize( + &mut self, + width: u32, + height: u32 + ) { + if width > 0 && height > 0 { + self.wgpuctx.resize(width, height); + self.tactical_map.resize(&self.wgpuctx, width, height); + self.window.request_redraw(); + } + } + + pub fn size( + &self) + -> winit::dpi::PhysicalSize { + self.window.inner_size() + } +} diff --git a/src/window/ui.rs b/src/window/ui.rs new file mode 100644 index 0000000..4d13740 --- /dev/null +++ b/src/window/ui.rs @@ -0,0 +1,96 @@ +use std::cell::RefCell; + +use cgmath::Vector3; + +use crate::{GameState, eguictx::EguiCtx, solar_system, timeman::{Second, TimeMan}}; + +#[derive(Default, Clone)] +pub struct GameWindowUiState +{ + pub current_system: Option, + pub camera_scale: f32, + pub camera_target: Option, + pub auto_time: Option, + pub do_auto_tick: bool +} + +impl GameWindowUiState +{ + pub fn render( + old_state: &GameWindowUiState, + game_state: &RefCell, + eguictx: &EguiCtx) + -> Self + { + let mut new_state = old_state.clone(); + + { GameWindowUiState::render_topbar(&mut new_state, game_state, eguictx); } + + new_state + } + + fn render_topbar( + state: &mut GameWindowUiState, + game_state: &RefCell, + eguictx: &EguiCtx) + { + let mut game_state = game_state.borrow_mut(); + + egui::TopBottomPanel::top("topbar").show( + eguictx.context(), + |ui| { + + ui.horizontal(|ui| { + ui.menu_button("File", |ui| { + + }); + }); + + ui.horizontal(|ui| { + let solar_systems = game_state.solar_systems(); + let selected_label = match state.current_system { + Some(id) => solar_systems[id].name(), + None => "" + }; + + egui::ComboBox::from_label("Current System") + .selected_text(selected_label) + .show_ui(ui, |ui| { + + for (i, system) in solar_systems.iter().enumerate() { + ui.selectable_value( + &mut state.current_system, + Some(i), + system.name() + ); + } + }); + }); + + ui.horizontal(|ui| { + ui.label(format!("Time: {}", TimeMan::format_duration(game_state.timeman().seconds()))); + + let button_seconds = [1, 5, 30, 60, 60*5, 60*30, 60*60, 60*60*24, 60*60*24*5, 60*60*24*30]; + let selected_button = state.auto_time; + + button_seconds.iter().for_each(|&seconds| { + ui.vertical(|ui| { + let auto_selected = match selected_button { + Some(o) => o == seconds, + None => false + }; + let label = TimeMan::format_duration(seconds); + + if ui.button(label.clone()).clicked() { + game_state.timeman_mut().advance(seconds); + } + + if ui.add(egui::Button::new(label.clone()).selected(auto_selected)).clicked() { + state.auto_time = Some(seconds); + } + }); + }); + }); + }); + } +} -- cgit v1.2.3