summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/canvas.rs176
-rw-r--r--src/eguictx.rs143
-rw-r--r--src/known_stars.rs24
-rw-r--r--src/main.rs178
-rw-r--r--src/solar_system.rs161
-rw-r--r--src/tacmap.rs158
-rw-r--r--src/tacmap/camera.rs272
-rw-r--r--src/tacmap/render.rs233
-rw-r--r--src/texture.rs96
-rw-r--r--src/timeman.rs69
-rw-r--r--src/vertex.rs33
-rw-r--r--src/wgpuctx/mod.rs245
-rw-r--r--src/wgpuctx/pipeline.rs121
-rw-r--r--src/window.rs148
-rw-r--r--src/window/ui.rs96
15 files changed, 2153 insertions, 0 deletions
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<f32>,
+ size: winit::dpi::LogicalSize<f32>
+}
+
+impl Canvas
+{
+ fn create_canvas_texture(
+ wgpuctx: &WgpuCtx,
+ canvas_size: winit::dpi::LogicalSize<f32>,
+ window_size: winit::dpi::PhysicalSize<u32>)
+ -> CanvasTexture
+ {
+ let canvas_physical_size = winit::dpi::PhysicalSize::<u32>::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<u32>,
+ position: winit::dpi::LogicalPosition<f32>,
+ size: winit::dpi::LogicalSize<f32>
+ ) -> Result<Self, ()> {
+
+ 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<u32>)
+ -> Self
+ {
+ let u32_size = std::mem::size_of::<u32>() 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<u32>)
+ -> 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<GameWindow>,
+ wgpu_instance: wgpu::Instance,
+
+ last_render_time: Instant,
+ game_state: Option<RefCell<GameState>>
+}
+
+struct GameState
+{
+ timeman: TimeMan,
+ solar_systems: Vec<SolarSystem>
+}
+
+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<Kilometers>)>
+}
+
+pub struct SolarSystem
+{
+ name: String,
+ bodies: Vec<OrbitalBody>,
+}
+
+impl SolarSystem
+{
+ pub fn new_from_csv(
+ data: &'static str)
+ -> Result<Self, Box<dyn Error>> {
+ let data_reader = stringreader::StringReader::new(data);
+ let mut body_reader = csv::Reader::from_reader(data_reader);
+
+ let mut bodies = Vec::<OrbitalBody>::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<Self, Box<dyn Error>> {
+ 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<cgmath::Point3<Kilometers>>
+ {
+ 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<Kilometers> {
+ 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<f32>,
+ size: winit::dpi::LogicalSize<f32>)
+ -> 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::<f32>::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<GameState>,
+ 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<GameState>,
+ 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<GameState>,
+ current_system: Option<SystemId>)
+ -> 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<f32> = 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<f32>,
+ pitch: Rad<f32>,
+ yaw: Rad<f32>,
+ scale: f32,
+
+ target: Option<BodyId>,
+
+ buffer: wgpu::Buffer,
+ staging_buffer: wgpu::Buffer,
+ bindgroup: wgpu::BindGroup
+}
+
+pub struct CameraController
+{
+ position_pos_delta: Vector3<f32>,
+ position_neg_delta: Vector3<f32>,
+ rotation_pos_delta: Vector2<f32>,
+ rotation_neg_delta: Vector2<f32>,
+ scale_delta: Vector2<f32>,
+}
+
+#[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<f32>,
+ clip: (f32, f32)
+}
+
+impl Camera
+{
+ pub fn new<
+ V: Into<Point3<f32>>,
+ Y: Into<Rad<f32>>,
+ P: Into<Rad<f32>>
+ >(
+ 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::<CameraUniform>();
+ 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<f32> {
+
+ 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<Rad<f32>>
+ >(
+ 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<f32> {
+ 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<Kilometers>,
+ 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<Second>,
+
+ 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::<BodyInstanceRaw>();
+
+ 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<dyn Error>>
+ {
+ //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::<Vec<_>>();
+
+ 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::<BodyInstanceRaw>() 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<Second>
+}
+
+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::<Self>() 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<wgpu::SurfaceTexture>,
+ 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<Window>,
+ ) -> 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<wgpu::TextureView, wgpu::SurfaceError>
+ {
+ 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<wgpu::PipelineCompilationOptions<'a>>,
+ fragment_comp_options: Option<wgpu::PipelineCompilationOptions<'a>>,
+
+ vertex_buffer_layouts: Vec<wgpu::VertexBufferLayout<'a>>
+}
+
+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<winit::window::Window>,
+ 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<GameWindow, ()>
+ {
+ 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<GameState>,
+ dt: Duration)
+ {
+ self.tactical_map.update(game_state, &mut self.ui_state, dt);
+ }
+
+ pub fn keyboard_input(
+ &mut self,
+ game_state: &RefCell<GameState>,
+ 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<GameState>)
+ -> 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<u32> {
+ 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<solar_system::SystemId>,
+ pub camera_scale: f32,
+ pub camera_target: Option<solar_system::BodyId>,
+ pub auto_time: Option<Second>,
+ pub do_auto_tick: bool
+}
+
+impl GameWindowUiState
+{
+ pub fn render(
+ old_state: &GameWindowUiState,
+ game_state: &RefCell<GameState>,
+ 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<GameState>,
+ 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);
+ }
+ });
+ });
+ });
+ });
+ }
+}