summaryrefslogtreecommitdiffstats
path: root/src/tacmap
diff options
context:
space:
mode:
authorJon Santmyer <jon@jonsantmyer.com>2026-04-15 16:53:58 -0400
committerJon Santmyer <jon@jonsantmyer.com>2026-04-15 16:53:58 -0400
commitb5ced3af46c96ceb959fbbf1addfeba3bd4f76d5 (patch)
tree06d06fc9562dc99eede655b53635576f67c37e3b /src/tacmap
downloadsystemic4x-b5ced3af46c96ceb959fbbf1addfeba3bd4f76d5.tar.gz
systemic4x-b5ced3af46c96ceb959fbbf1addfeba3bd4f76d5.tar.bz2
systemic4x-b5ced3af46c96ceb959fbbf1addfeba3bd4f76d5.zip
first commit. working body rendering
Diffstat (limited to 'src/tacmap')
-rw-r--r--src/tacmap/camera.rs272
-rw-r--r--src/tacmap/render.rs233
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
+ }
+ ]
+ }
+ }
+}