use cgmath::{Point2, Point3, Vector2, Vector3}; use galactica_content::Content; use rand::{self, Rng}; use crate::{ datastructs::RenderState, vertexbuffer::{types::StarfieldInstance, BufferObject}, }; 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: 0, } } pub fn regenerate(&mut self, ct: &Content) { // TODO: save seed in system, regenerate on jump let mut rng = rand::thread_rng(); let sz = ct.get_config().starfield_size as f32 / 2.0; self.stars = (0..ct.get_config().starfield_count) .map(|_| StarfieldStar { pos: Point3 { x: rng.gen_range(-sz..=sz), y: rng.gen_range(-sz..=sz), z: rng.gen_range( ct.get_config().starfield_min_dist..=ct.get_config().starfield_max_dist, ), }, size: rng.gen_range( ct.get_config().starfield_min_size..ct.get_config().starfield_max_size, ), tint: Vector2 { x: rng.gen_range(0.0..=1.0), y: rng.gen_range(0.0..=1.0), }, }) .collect(); } pub fn update_buffer(&mut self, ct: &Content, state: &mut RenderState) { let sz = ct.get_config().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((state.window_aspect, 1.0)) * ct.get_config().zoom_max; // Parallax correction. // Also, adjust v for mod to work properly // (v is centered at 0) let v: Point2 = clip_nw * ct.get_config().starfield_min_dist; 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) * ct.get_config().starfield_count as i32) > ct.get_config().starfield_instance_limit as i32 { nw_tile -= Vector2::from((1, 1)); } // Add all tiles to buffer self.instance_count = 0; // Keep track of buffer index 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 { state.queue.write_buffer( &state.vertex_buffers.starfield.instances, StarfieldInstance::SIZE * self.instance_count as u64, bytemuck::cast_slice(&[StarfieldInstance { position: (s.pos + offset).into(), size: s.size, tint: s.tint.into(), }]), ); self.instance_count += 1; // instance_count is guaranteed to stay within buffer limits, // this is guaranteed by previous checks. } } } } }