diff options
Diffstat (limited to 'src/tacmap')
| -rw-r--r-- | src/tacmap/camera.rs | 272 | ||||
| -rw-r--r-- | src/tacmap/render.rs | 233 |
2 files changed, 505 insertions, 0 deletions
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 + } + ] + } + } +} |
