Starfield cleanup

master
Mark 2024-01-01 09:45:27 -08:00
parent 32101e73b8
commit bcc57fc9a8
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
8 changed files with 149 additions and 140 deletions

1
Cargo.lock generated
View File

@ -614,6 +614,7 @@ dependencies = [
"cgmath", "cgmath",
"galactica-content", "galactica-content",
"image", "image",
"rand",
"wgpu", "wgpu",
"winit", "winit",
] ]

View File

@ -4,11 +4,13 @@ version = "0.0.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
# Internal crates
galactica-content = { path = "../content" } galactica-content = { path = "../content" }
# Misc helpers # Misc helpers
anyhow = "1.0" anyhow = "1.0"
cgmath = "0.18.0" cgmath = "0.18.0"
rand = "0.8.5"
# Files # Files
image = { version = "0.24", features = ["png"] } image = { version = "0.24", features = ["png"] }
# Graphics # Graphics

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector2, Vector3}; use cgmath::{Deg, EuclideanSpace, Matrix4, Point2, Vector3};
use std::{iter, rc::Rc}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
use winit::{self, dpi::LogicalSize, window::Window}; use winit::{self, dpi::LogicalSize, window::Window};
@ -11,13 +11,14 @@ use crate::{
globaldata::{GlobalData, GlobalDataContent}, globaldata::{GlobalData, GlobalDataContent},
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
sprite::ObjectSubSprite, sprite::ObjectSubSprite,
starfield::Starfield,
texturearray::TextureArray, texturearray::TextureArray,
vertexbuffer::{ vertexbuffer::{
consts::{SPRITE_INDICES, SPRITE_VERTICES}, consts::{SPRITE_INDICES, SPRITE_VERTICES},
types::{SpriteInstance, StarfieldInstance, TexturedVertex}, types::{SpriteInstance, StarfieldInstance, TexturedVertex},
VertexBuffer, VertexBuffer,
}, },
ObjectSprite, StarfieldStar, UiSprite, ObjectSprite, UiSprite,
}; };
/// A high-level GPU wrapper. Consumes game state, /// A high-level GPU wrapper. Consumes game state,
@ -38,8 +39,8 @@ pub struct GPUState {
sprite_pipeline: wgpu::RenderPipeline, sprite_pipeline: wgpu::RenderPipeline,
starfield_pipeline: wgpu::RenderPipeline, starfield_pipeline: wgpu::RenderPipeline,
starfield_count: u32,
starfield: Starfield,
texture_array: TextureArray, texture_array: TextureArray,
global_data: GlobalData, global_data: GlobalData,
vertex_buffers: VertexBuffers, vertex_buffers: VertexBuffers,
@ -168,6 +169,9 @@ impl GPUState {
.set_bind_group_layouts(bind_group_layouts) .set_bind_group_layouts(bind_group_layouts)
.build(); .build();
let mut starfield = Starfield::new();
starfield.regenerate();
return Ok(Self { return Ok(Self {
device, device,
config, config,
@ -181,10 +185,10 @@ impl GPUState {
sprite_pipeline, sprite_pipeline,
starfield_pipeline, starfield_pipeline,
starfield,
texture_array, texture_array,
global_data, global_data,
vertex_buffers, vertex_buffers,
starfield_count: 0,
}); });
} }
@ -195,7 +199,7 @@ impl GPUState {
/// Update window size. /// Update window size.
/// This should be called whenever our window is resized. /// This should be called whenever our window is resized.
pub fn resize(&mut self, starfield: &Vec<StarfieldStar>) { pub fn resize(&mut self) {
let new_size = self.window.inner_size(); let new_size = self.window.inner_size();
if new_size.width > 0 && new_size.height > 0 { if new_size.width > 0 && new_size.height > 0 {
self.window_size = new_size; self.window_size = new_size;
@ -204,7 +208,7 @@ impl GPUState {
self.config.height = new_size.height; self.config.height = new_size.height;
self.surface.configure(&self.device, &self.config); 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`. /// 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. /// Will panic if STARFIELD_INSTANCE_LIMIT is exceeded.
/// ///
/// Starfield data rarely changes, so this is called only when it's needed. /// Starfield data rarely changes, so this is called only when it's needed.
pub fn update_starfield_buffer(&mut self, starfield: &Vec<StarfieldStar>) { pub fn update_starfield_buffer(&mut self) {
let sz = consts_main::STARFIELD_SIZE as f32;
// Compute window size in starfield tiles
let mut nw_tile: Point2<i32> = {
// 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<f32> = clip_nw * consts_main::STARFIELD_Z_MIN;
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
#[rustfmt::skip]
// Compute m = fmod(x, sz)
let m: Vector2<f32> = (
(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;
self.queue.write_buffer( self.queue.write_buffer(
&self.vertex_buffers.starfield.instances, &self.vertex_buffers.starfield.instances,
0, 0,
bytemuck::cast_slice(&instances), bytemuck::cast_slice(&self.starfield.make_instances(self.window_aspect)),
); );
} }
@ -570,7 +508,11 @@ impl GPUState {
// Starfield pipeline // Starfield pipeline
self.vertex_buffers.starfield.set_in_pass(&mut render_pass); self.vertex_buffers.starfield.set_in_pass(&mut render_pass);
render_pass.set_pipeline(&self.starfield_pipeline); 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 // Sprite pipeline
self.vertex_buffers.sprite.set_in_pass(&mut render_pass); self.vertex_buffers.sprite.set_in_pass(&mut render_pass);

View File

@ -12,28 +12,13 @@ mod globaldata;
mod gpustate; mod gpustate;
mod pipeline; mod pipeline;
mod sprite; mod sprite;
mod starfield;
mod texturearray; mod texturearray;
mod vertexbuffer; mod vertexbuffer;
// TODO: remove // TODO: remove
mod consts_main; mod consts_main;
use cgmath::{Point3, Vector2};
use galactica_content as content; use galactica_content as content;
pub use gpustate::GPUState; pub use gpustate::GPUState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, UiSprite}; 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<f32>,
/// 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<f32>,
}

View File

@ -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<f32>,
/// 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<f32>,
}
pub(crate) struct Starfield {
stars: Vec<StarfieldStar>,
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<StarfieldInstance> {
let sz = consts_main::STARFIELD_SIZE as f32;
// Compute window size in starfield tiles
let mut nw_tile: Point2<i32> = {
// 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<f32> = clip_nw * consts_main::STARFIELD_Z_MIN;
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
#[rustfmt::skip]
// Compute m = fmod(x, sz)
let m: Vector2<f32> = (
(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;
}
}

View File

@ -1,30 +1,6 @@
pub const ZOOM_MIN: f32 = 200.0; pub const ZOOM_MIN: f32 = 200.0;
pub const ZOOM_MAX: f32 = 2000.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 /// Name of starfield texture
pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield"; pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield";

View File

@ -1,37 +1,18 @@
use cgmath::{Point3, Vector2}; use galactica_render::ObjectSprite;
use galactica_render::{ObjectSprite, StarfieldStar};
use rand::{self, Rng};
use super::SystemObject; use super::SystemObject;
use crate::{consts, content}; use crate::content;
pub struct System { pub struct System {
pub name: String, pub name: String,
bodies: Vec<SystemObject>, bodies: Vec<SystemObject>,
pub starfield: Vec<StarfieldStar>,
} }
impl System { impl System {
pub fn new(ct: &content::System) -> Self { 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 { let mut s = System {
name: ct.name.clone(), name: ct.name.clone(),
bodies: Vec::new(), 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 { for o in &ct.objects {

View File

@ -28,7 +28,7 @@ fn main() -> Result<()> {
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?; let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?;
let mut game = game::Game::new(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| { event_loop.run(move |event, _, control_flow| {
match event { match event {
@ -40,7 +40,7 @@ fn main() -> Result<()> {
&game.get_ui_sprites(), &game.get_ui_sprites(),
) { ) {
Ok(_) => {} Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => gpu.resize(&game.system.starfield), Err(wgpu::SurfaceError::Lost) => gpu.resize(),
Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit,
// All other errors (Outdated, Timeout) should be resolved by the next frame // All other errors (Outdated, Timeout) should be resolved by the next frame
Err(e) => eprintln!("{:?}", e), Err(e) => eprintln!("{:?}", e),
@ -76,10 +76,10 @@ fn main() -> Result<()> {
game.process_scroll(delta, phase); game.process_scroll(delta, phase);
} }
WindowEvent::Resized(_) => { WindowEvent::Resized(_) => {
gpu.resize(&game.system.starfield); gpu.resize();
} }
WindowEvent::ScaleFactorChanged { .. } => { WindowEvent::ScaleFactorChanged { .. } => {
gpu.resize(&game.system.starfield); gpu.resize();
} }
_ => {} _ => {}
}, },