diff --git a/src/main.rs b/src/main.rs index 24c1137..dc87eb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,6 +26,9 @@ pub const STARFIELD_COUNT: u64 = 100; // will completely cover a (square) screen. This depends on zoom! pub const STARFIELD_SIZE: u64 = STARFIELD_PARALLAX_MAX as u64 * 500; +pub const ZOOM_MIN: Pfloat = 200.0; +pub const ZOOM_MAX: Pfloat = 2000.0; + use crate::{ doodad::Doodad, inputstatus::InputStatus, @@ -84,9 +87,6 @@ struct Game { } impl Game { - const ZOOM_MIN: Pfloat = 200.0; - const ZOOM_MAX: Pfloat = 2000.0; - fn new() -> Self { Game { last_update: Instant::now(), @@ -128,8 +128,7 @@ impl Game { } if self.input.v_scroll != 0.0 { - self.camera.zoom = - (self.camera.zoom + self.input.v_scroll).clamp(Self::ZOOM_MIN, Self::ZOOM_MAX); + self.camera.zoom = (self.camera.zoom + self.input.v_scroll).clamp(ZOOM_MIN, ZOOM_MAX); self.input.v_scroll = 0.0; } @@ -165,7 +164,7 @@ pub async fn run() -> Result<()> { let mut gpu = GPUState::new(window).await?; let mut game = Game::new(); - gpu.on_new_system(&game); + gpu.update_starfield_buffer(&game); event_loop.run(move |event, _, control_flow| { match event { @@ -175,7 +174,7 @@ pub async fn run() -> Result<()> { match gpu.render(&game) { Ok(_) => {} // Reconfigure the surface if lost - Err(wgpu::SurfaceError::Lost) => gpu.resize(gpu.window_size), + Err(wgpu::SurfaceError::Lost) => gpu.resize(&game, gpu.window_size), // The system is out of memory, we should probably quit Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, // All other errors (Outdated, Timeout) should be resolved by the next frame @@ -205,17 +204,17 @@ pub async fn run() -> Result<()> { .. } => game.process_key(state, key), WindowEvent::MouseInput { state, button, .. } => { - game.process_click(state, button) + game.process_click(state, button); } WindowEvent::MouseWheel { delta, phase, .. } => { - game.process_scroll(delta, phase) + game.process_scroll(delta, phase); } WindowEvent::Resized(physical_size) => { - gpu.resize(*physical_size); + gpu.resize(&game, *physical_size); } WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { // new_inner_size is &&mut so we have to dereference it twice - gpu.resize(**new_inner_size); + gpu.resize(&game, **new_inner_size); } _ => {} } diff --git a/src/render/gpustate.rs b/src/render/gpustate.rs index de4296f..c3089cb 100644 --- a/src/render/gpustate.rs +++ b/src/render/gpustate.rs @@ -5,7 +5,7 @@ use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::PhysicalSize, window::Window}; -use crate::{Game, STARFIELD_COUNT, STARFIELD_SIZE}; +use crate::{Game, STARFIELD_COUNT, STARFIELD_PARALLAX_MIN, STARFIELD_SIZE, ZOOM_MAX}; use super::{ globaldata::{GlobalData, GlobalDataContent}, @@ -46,6 +46,8 @@ impl GPUState { // We can draw at most this many sprites on the screen. // TODO: compile-time option pub const SPRITE_INSTANCE_LIMIT: u64 = 100; + + // Must be small enough to fit in an i32 pub const STARFIELD_INSTANCE_LIMIT: u64 = STARFIELD_COUNT * 9; pub async fn new(window: Window) -> Result { @@ -188,7 +190,7 @@ impl GPUState { &self.window } - pub fn resize(&mut self, new_size: PhysicalSize) { + pub fn resize(&mut self, game: &Game, new_size: PhysicalSize) { if new_size.width > 0 && new_size.height > 0 { self.window_size = new_size; self.window_aspect = new_size.width as f32 / new_size.height as f32; @@ -196,6 +198,7 @@ impl GPUState { self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); } + self.update_starfield_buffer(game) } pub fn update(&mut self) {} @@ -253,27 +256,56 @@ impl GPUState { /// Make a StarfieldInstance for each star that needs to be drawn. /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded. /// - /// This is only called inside self.render() - fn make_starfield_instances(&mut self, game: &Game) -> Vec { - let mut instances = Vec::new(); + /// Starfield data rarely changes, so this is called only when it's needed. + pub fn update_starfield_buffer(&mut self, game: &Game) { let sz = STARFIELD_SIZE as f32; - for offset in [ - Vector2 { x: 0.0, y: 0.0 }, - Vector2 { x: 0.0, y: sz }, - Vector2 { x: sz, y: sz }, - Vector2 { x: sz, y: 0.0 }, - Vector2 { x: sz, y: -sz }, - Vector2 { x: 0.0, y: -sz }, - Vector2 { x: -sz, y: -sz }, - Vector2 { x: -sz, y: 0.0 }, - Vector2 { x: -sz, y: sz }, - ] { - for s in &game.system.starfield { - instances.push(StarfieldInstance { - position: (s.pos + offset).into(), - parallax: s.parallax, - height: s.height, - }) + + // Compute window size in starfield tiles + let mut nw_tile: Point2 = { + // Game coordinates (relative to camera) of nw corner of screen. + let clip_nw = Point2::from((self.window_aspect, 1.0)) * ZOOM_MAX / 2.0; + + // Adjust for parallax + let v: Point2 = clip_nw * STARFIELD_PARALLAX_MIN; + + // We don't need to adjust coordinates here, since clip_nw is always positive. + + #[rustfmt::skip] + // Compute m = fmod(x, sz) + let m: Vector2 = ( + (v.x - (v.x / sz).floor() * sz), + (v.y - (v.y / sz).floor() * sz) + ).into(); + + // Now, remainder and convert to "relative tile" coordinates + // ( where (0,0) is center tile, (0, 1) is north, etc) + let rel = (v - m) / sz; + + // relative coordinates of north-east tile + (rel.x.round() as i32, rel.y.round() as i32).into() + }; + + // Truncate tile grid to buffer size + // (The window won't be full of stars if we don't have a large enough instance limit) + while (nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) > Self::STARFIELD_INSTANCE_LIMIT as i32 { + nw_tile -= Vector2::from((1, 1)); + } + + // Add all tiles to buffer + let mut instances = Vec::new(); + for x in (-nw_tile.x)..=nw_tile.x { + for y in (-nw_tile.y)..=nw_tile.y { + let offset = Vector2 { + x: sz * x as f32, + y: sz * y as f32, + }; + for s in &game.system.starfield { + instances.push(StarfieldInstance { + position: (s.pos + offset).into(), + parallax: s.parallax, + height: s.height, + }) + } } } @@ -284,16 +316,10 @@ impl GPUState { } self.starfield_count = instances.len() as u32; - return instances; - } - - pub fn on_new_system(&mut self, game: &Game) { - // Create starfield instances - let starfield_instances = self.make_starfield_instances(game); self.queue.write_buffer( &self.vertex_buffers.starfield.instances, 0, - bytemuck::cast_slice(&starfield_instances), + bytemuck::cast_slice(&instances), ); } diff --git a/src/render/shaders/starfield.wgsl b/src/render/shaders/starfield.wgsl index 62dbf97..bcabc40 100644 --- a/src/render/shaders/starfield.wgsl +++ b/src/render/shaders/starfield.wgsl @@ -27,7 +27,7 @@ struct GlobalUniform { fn fmod(x: vec2, m: f32) -> vec2 { - return x - floor(x * (1.0 / m)) * m; + return x - floor(x / m) * m; } @vertex @@ -64,7 +64,7 @@ fn vertex_shader_main( // Translate pos = pos + ( // Don't forget to correct distance for screen aspect ratio too! - (camera_pos / (global.camera_zoom * instance.parallax)) + (camera_pos / (global.camera_zoom * (instance.parallax))) / vec2(global.window_aspect, 1.0) );