From bcc57fc9a8b85373b023956a1c60d4dafd829e98 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 1 Jan 2024 09:45:27 -0800 Subject: [PATCH] Starfield cleanup --- Cargo.lock | 1 + crates/render/Cargo.toml | 2 + crates/render/src/gpustate.rs | 92 +++++-------------------- crates/render/src/lib.rs | 17 +---- crates/render/src/starfield.rs | 122 +++++++++++++++++++++++++++++++++ src/consts.rs | 24 ------- src/game/system.rs | 23 +------ src/main.rs | 8 +-- 8 files changed, 149 insertions(+), 140 deletions(-) create mode 100644 crates/render/src/starfield.rs diff --git a/Cargo.lock b/Cargo.lock index 5f5d1c5..9866868 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -614,6 +614,7 @@ dependencies = [ "cgmath", "galactica-content", "image", + "rand", "wgpu", "winit", ] diff --git a/crates/render/Cargo.toml b/crates/render/Cargo.toml index 4a936af..a5a701a 100644 --- a/crates/render/Cargo.toml +++ b/crates/render/Cargo.toml @@ -4,11 +4,13 @@ version = "0.0.0" edition = "2021" [dependencies] +# Internal crates galactica-content = { path = "../content" } # Misc helpers anyhow = "1.0" cgmath = "0.18.0" +rand = "0.8.5" # Files image = { version = "0.24", features = ["png"] } # Graphics diff --git a/crates/render/src/gpustate.rs b/crates/render/src/gpustate.rs index 3aae2cd..a1f09c9 100644 --- a/crates/render/src/gpustate.rs +++ b/crates/render/src/gpustate.rs @@ -1,6 +1,6 @@ use anyhow::Result; use bytemuck; -use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector2, Vector3}; +use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3}; use std::{iter, rc::Rc}; use wgpu; use winit::{self, dpi::LogicalSize, window::Window}; @@ -11,13 +11,14 @@ use crate::{ globaldata::{GlobalData, GlobalDataContent}, pipeline::PipelineBuilder, sprite::ObjectSubSprite, + starfield::Starfield, texturearray::TextureArray, vertexbuffer::{ consts::{SPRITE_INDICES, SPRITE_VERTICES}, types::{SpriteInstance, StarfieldInstance, TexturedVertex}, VertexBuffer, }, - ObjectSprite, StarfieldStar, UiSprite, + ObjectSprite, UiSprite, }; /// A high-level GPU wrapper. Consumes game state, @@ -38,8 +39,8 @@ pub struct GPUState { sprite_pipeline: wgpu::RenderPipeline, starfield_pipeline: wgpu::RenderPipeline, - starfield_count: u32, + starfield: Starfield, texture_array: TextureArray, global_data: GlobalData, vertex_buffers: VertexBuffers, @@ -168,6 +169,9 @@ impl GPUState { .set_bind_group_layouts(bind_group_layouts) .build(); + let mut starfield = Starfield::new(); + starfield.regenerate(); + return Ok(Self { device, config, @@ -181,10 +185,10 @@ impl GPUState { sprite_pipeline, starfield_pipeline, + starfield, texture_array, global_data, vertex_buffers, - starfield_count: 0, }); } @@ -195,7 +199,7 @@ impl GPUState { /// Update window size. /// This should be called whenever our window is resized. - pub fn resize(&mut self, starfield: &Vec) { + pub fn resize(&mut self) { let new_size = self.window.inner_size(); if new_size.width > 0 && new_size.height > 0 { self.window_size = new_size; @@ -204,7 +208,7 @@ impl GPUState { self.config.height = new_size.height; self.surface.configure(&self.device, &self.config); } - self.update_starfield_buffer(starfield) + self.update_starfield_buffer() } /// Create a SpriteInstance for an object and add it to `instances`. @@ -417,77 +421,11 @@ impl GPUState { /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded. /// /// Starfield data rarely changes, so this is called only when it's needed. - pub fn update_starfield_buffer(&mut self, starfield: &Vec) { - let sz = consts_main::STARFIELD_SIZE as f32; - - // Compute window size in starfield tiles - let mut nw_tile: Point2 = { - // Game coordinates (relative to camera) of nw corner of screen. - // TODO: we probably don't need to re-compute this on resize - let clip_nw = Point2::from((self.window_aspect, 1.0)) * consts_main::ZOOM_MAX; - - // Parallax correction. - // Also, adjust v for mod to work properly - // (v is centered at 0) - let v: Point2 = clip_nw * consts_main::STARFIELD_Z_MIN; - let v_adj: Point2 = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); - - #[rustfmt::skip] - // Compute m = fmod(x, sz) - let m: Vector2 = ( - (v_adj.x - (v_adj.x / sz).floor() * sz) - (sz / 2.0), - (v_adj.y - (v_adj.y / sz).floor() * sz) - (sz / 2.0) - ).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() - }; - - // We need to cover the window with stars, - // but we also need a one-wide buffer to account for motion. - nw_tile += Vector2::from((1, 1)); - - // Truncate tile grid to buffer size - // (The window won't be full of stars if our instance limit is too small) - while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * consts_main::STARFIELD_COUNT as i32) - > 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 = Vector3 { - x: sz * x as f32, - y: sz * y as f32, - z: 0.0, - }; - for s in starfield { - instances.push(StarfieldInstance { - position: (s.pos + offset).into(), - size: s.size, - tint: s.tint.into(), - }) - } - } - } - - // Enforce starfield limit - if instances.len() as u64 > STARFIELD_INSTANCE_LIMIT { - unreachable!("Starfield limit exceeded!") - } - - self.starfield_count = instances.len() as u32; + pub fn update_starfield_buffer(&mut self) { self.queue.write_buffer( &self.vertex_buffers.starfield.instances, 0, - bytemuck::cast_slice(&instances), + bytemuck::cast_slice(&self.starfield.make_instances(self.window_aspect)), ); } @@ -570,7 +508,11 @@ impl GPUState { // Starfield pipeline self.vertex_buffers.starfield.set_in_pass(&mut render_pass); render_pass.set_pipeline(&self.starfield_pipeline); - render_pass.draw_indexed(0..SPRITE_INDICES.len() as u32, 0, 0..self.starfield_count); + render_pass.draw_indexed( + 0..SPRITE_INDICES.len() as u32, + 0, + 0..self.starfield.instance_count, + ); // Sprite pipeline self.vertex_buffers.sprite.set_in_pass(&mut render_pass); diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index cf1ceb5..e1685e9 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -12,28 +12,13 @@ mod globaldata; mod gpustate; mod pipeline; mod sprite; +mod starfield; mod texturearray; mod vertexbuffer; // TODO: remove mod consts_main; -use cgmath::{Point3, Vector2}; use galactica_content as content; pub use gpustate::GPUState; pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite}; - -/// TODO: this shouldn't be here -pub struct StarfieldStar { - /// Star coordinates, in world space. - /// These are relative to the center of a starfield tile. - pub pos: Point3, - - /// Height in game units. - /// Will be scaled for zoom, but not for distance. - pub size: f32, - - /// Color/brightness variation. Random between 0 and 1. - /// Used in starfield shader. - pub tint: Vector2, -} diff --git a/crates/render/src/starfield.rs b/crates/render/src/starfield.rs new file mode 100644 index 0000000..6f9f347 --- /dev/null +++ b/crates/render/src/starfield.rs @@ -0,0 +1,122 @@ +use cgmath::{Point2, Point3, Vector2, Vector3}; +use rand::{self, Rng}; + +use crate::{consts, consts_main, vertexbuffer::types::StarfieldInstance}; + +pub(crate) struct StarfieldStar { + /// Star coordinates, in world space. + /// These are relative to the center of a starfield tile. + pub pos: Point3, + + /// Height in game units. + /// Will be scaled for zoom, but not for distance. + pub size: f32, + + /// Color/brightness variation. Random between 0 and 1. + /// Used in starfield shader. + pub tint: Vector2, +} + +pub(crate) struct Starfield { + stars: Vec, + pub instance_count: u32, +} + +impl Starfield { + pub fn new() -> Self { + Self { + stars: Vec::new(), + instance_count: 0u32, + } + } + + pub fn regenerate(&mut self) { + // TODO: save seed in system, regenerate on jump + let mut rng = rand::thread_rng(); + let sz = consts_main::STARFIELD_SIZE as f32 / 2.0; + self.stars = (0..consts_main::STARFIELD_COUNT) + .map(|_| StarfieldStar { + pos: Point3 { + x: rng.gen_range(-sz..=sz), + y: rng.gen_range(-sz..=sz), + z: rng.gen_range(consts_main::STARFIELD_Z_MIN..consts_main::STARFIELD_Z_MAX), + }, + size: rng + .gen_range(consts_main::STARFIELD_SIZE_MIN..consts_main::STARFIELD_SIZE_MAX), + tint: Vector2 { + x: rng.gen_range(0.0..=1.0), + y: rng.gen_range(0.0..=1.0), + }, + }) + .collect(); + } + + pub fn make_instances(&mut self, aspect: f32) -> Vec { + let sz = consts_main::STARFIELD_SIZE as f32; + + // 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((aspect, 1.0)) * consts_main::ZOOM_MAX; + + // Parallax correction. + // Also, adjust v for mod to work properly + // (v is centered at 0) + let v: Point2 = clip_nw * consts_main::STARFIELD_Z_MIN; + let v_adj: Point2 = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); + + #[rustfmt::skip] + // Compute m = fmod(x, sz) + let m: Vector2 = ( + (v_adj.x - (v_adj.x / sz).floor() * sz) - (sz / 2.0), + (v_adj.y - (v_adj.y / sz).floor() * sz) - (sz / 2.0) + ).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() + }; + + // We need to cover the window with stars, + // but we also need a one-wide buffer to account for motion. + nw_tile += Vector2::from((1, 1)); + + // Truncate tile grid to buffer size + // (The window won't be full of stars if our instance limit is too small) + while ((nw_tile.x * 2 + 1) * (nw_tile.y * 2 + 1) * consts_main::STARFIELD_COUNT as i32) + > consts::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 = Vector3 { + x: sz * x as f32, + y: sz * y as f32, + z: 0.0, + }; + for s in &self.stars { + instances.push(StarfieldInstance { + position: (s.pos + offset).into(), + size: s.size, + tint: s.tint.into(), + }) + } + } + } + + // Enforce starfield limit + if instances.len() as u64 > consts::STARFIELD_INSTANCE_LIMIT { + unreachable!("Starfield limit exceeded!") + } + self.instance_count = instances.len() as u32; + + return instances; + } +} diff --git a/src/consts.rs b/src/consts.rs index 4e99ea8..de0bbd6 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -1,30 +1,6 @@ pub const ZOOM_MIN: f32 = 200.0; pub const ZOOM_MAX: f32 = 2000.0; -/// Z-axis range for starfield stars -/// This does not affect scale. -pub const STARFIELD_Z_MIN: f32 = 100.0; -pub const STARFIELD_Z_MAX: f32 = 200.0; - -/// Size range for starfield stars, in game units. -/// This is scaled for zoom, but NOT for distance. -pub const STARFIELD_SIZE_MIN: f32 = 0.2; -pub const STARFIELD_SIZE_MAX: f32 = 1.8; - -/// Size of a square starfield tile, in game units. -/// A tile of size STARFIELD_Z_MAX * screen-size-in-game-units -/// will completely cover a (square) screen. This depends on zoom! -/// -/// Use a value smaller than zoom_max for debug. -pub const STARFIELD_SIZE: u64 = STARFIELD_Z_MAX as u64 * ZOOM_MAX as u64; - -/// Average number of stars per game unit -pub const STARFIELD_DENSITY: f64 = 0.01; - -/// Number of stars in one starfield tile -/// Must fit inside an i32 -pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64; - /// Name of starfield texture pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield"; diff --git a/src/game/system.rs b/src/game/system.rs index 4e9bdf4..47098bc 100644 --- a/src/game/system.rs +++ b/src/game/system.rs @@ -1,37 +1,18 @@ -use cgmath::{Point3, Vector2}; -use galactica_render::{ObjectSprite, StarfieldStar}; -use rand::{self, Rng}; +use galactica_render::ObjectSprite; use super::SystemObject; -use crate::{consts, content}; +use crate::content; pub struct System { pub name: String, bodies: Vec, - pub starfield: Vec, } impl System { pub fn new(ct: &content::System) -> Self { - let mut rng = rand::thread_rng(); - let sz = consts::STARFIELD_SIZE as f32 / 2.0; let mut s = System { name: ct.name.clone(), bodies: Vec::new(), - starfield: (0..consts::STARFIELD_COUNT) - .map(|_| StarfieldStar { - pos: Point3 { - x: rng.gen_range(-sz..=sz), - y: rng.gen_range(-sz..=sz), - z: rng.gen_range(consts::STARFIELD_Z_MIN..consts::STARFIELD_Z_MAX), - }, - size: rng.gen_range(consts::STARFIELD_SIZE_MIN..consts::STARFIELD_SIZE_MAX), - tint: Vector2 { - x: rng.gen_range(0.0..=1.0), - y: rng.gen_range(0.0..=1.0), - }, - }) - .collect(), }; for o in &ct.objects { diff --git a/src/main.rs b/src/main.rs index 69b09ef..66fd67e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -28,7 +28,7 @@ fn main() -> Result<()> { let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?; let mut game = game::Game::new(content); - gpu.update_starfield_buffer(&game.system.starfield); + gpu.update_starfield_buffer(); event_loop.run(move |event, _, control_flow| { match event { @@ -40,7 +40,7 @@ fn main() -> Result<()> { &game.get_ui_sprites(), ) { Ok(_) => {} - Err(wgpu::SurfaceError::Lost) => gpu.resize(&game.system.starfield), + Err(wgpu::SurfaceError::Lost) => gpu.resize(), Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, // All other errors (Outdated, Timeout) should be resolved by the next frame Err(e) => eprintln!("{:?}", e), @@ -76,10 +76,10 @@ fn main() -> Result<()> { game.process_scroll(delta, phase); } WindowEvent::Resized(_) => { - gpu.resize(&game.system.starfield); + gpu.resize(); } WindowEvent::ScaleFactorChanged { .. } => { - gpu.resize(&game.system.starfield); + gpu.resize(); } _ => {} },