Renamed "parallax" into 3D coordinates,

polished starfield hiding.
master
Mark 2023-12-25 15:56:27 -08:00
parent abd41af202
commit a5b3932e9d
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
13 changed files with 192 additions and 111 deletions

View File

@ -4,9 +4,8 @@ name = "12 Autumn above"
[object.star] [object.star]
sprite = "star::star" sprite = "star::star"
position = [0.0, 0.0] position = [0.0, 0.0, 20.0]
size = 1000 size = 1000
parallax = 20.0
[object.earth] [object.earth]
@ -14,8 +13,8 @@ sprite = "planet::earth"
position.center = "star" position.center = "star"
position.radius = 4000 position.radius = 4000
position.angle = 0 position.angle = 0
position.z = 10.0
size = 1000 size = 1000
parallax = 10.0
[object.luna] [object.luna]
@ -23,6 +22,6 @@ sprite = "planet::luna"
position.center = "earth" position.center = "earth"
position.radius = 1600 position.radius = 1600
position.angle = 135 position.angle = 135
position.z = 7.8
size = 500 size = 500
angle = -45 angle = -45
parallax = 7.8

View File

@ -3,19 +3,29 @@ use crate::physics::Pfloat;
pub const ZOOM_MIN: Pfloat = 200.0; pub const ZOOM_MIN: Pfloat = 200.0;
pub const ZOOM_MAX: Pfloat = 2000.0; pub const ZOOM_MAX: Pfloat = 2000.0;
// Z-axis range for starfield stars /// Z-axis range for starfield stars
pub const STARFIELD_PARALLAX_MIN: f32 = 100.0; /// This does not affect scale.
pub const STARFIELD_PARALLAX_MAX: f32 = 200.0; pub const STARFIELD_Z_MIN: f32 = 100.0;
// Size of a square starfield tile, in game units. pub const STARFIELD_Z_MAX: f32 = 200.0;
// A tile of size PARALLAX_MAX * screen-size-in-game-units
// will completely cover a (square) screen. This depends on zoom! /// Size range for starfield stars, in game units.
// /// This is scaled for zoom, but NOT for distance.
// Use a value smaller than zoom_max for debug. pub const STARFIELD_SIZE_MIN: f32 = 0.2;
pub const STARFIELD_SIZE: u64 = STARFIELD_PARALLAX_MAX as u64 * ZOOM_MAX as u64; pub const STARFIELD_SIZE_MAX: f32 = 1.8;
// Average number of stars per game unit
/// 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; pub const STARFIELD_DENSITY: f64 = 0.01;
// Number of stars in one starfield tile
// Must fit inside an i32 /// 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; pub const STARFIELD_COUNT: u64 = (STARFIELD_SIZE as f64 * STARFIELD_DENSITY) as u64;
/// Root directory of game content
pub const CONTENT_ROOT: &'static str = "./content"; pub const CONTENT_ROOT: &'static str = "./content";

View File

@ -1,5 +1,5 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Result};
use cgmath::{Deg, Point2}; use cgmath::{Deg, Point3};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::physics::{Pfloat, Polar}; use crate::physics::{Pfloat, Polar};
@ -27,7 +27,6 @@ pub(in crate::content) mod toml {
pub position: Position, pub position: Position,
pub size: Pfloat, pub size: Pfloat,
pub parallax: Pfloat,
pub radius: Option<Pfloat>, pub radius: Option<Pfloat>,
pub angle: Option<Pfloat>, pub angle: Option<Pfloat>,
@ -37,24 +36,25 @@ pub(in crate::content) mod toml {
#[serde(untagged)] #[serde(untagged)]
pub enum Position { pub enum Position {
Polar(PolarCoords), Polar(PolarCoords),
Cartesian(Coordinates), Cartesian(CoordinatesThree),
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct PolarCoords { pub struct PolarCoords {
pub center: Coordinates, pub center: CoordinatesTwo,
pub radius: Pfloat, pub radius: Pfloat,
pub angle: Pfloat, pub angle: Pfloat,
pub z: Pfloat,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum Coordinates { pub enum CoordinatesTwo {
Label(String), Label(String),
Coords([Pfloat; 2]), Coords([Pfloat; 2]),
} }
impl ToString for Coordinates { impl ToString for CoordinatesTwo {
fn to_string(&self) -> String { fn to_string(&self) -> String {
match self { match self {
Self::Label(s) => s.to_owned(), Self::Label(s) => s.to_owned(),
@ -62,6 +62,44 @@ pub(in crate::content) mod toml {
} }
} }
} }
impl CoordinatesTwo {
/// Transform a CoordinatesThree into a CoordinatesTwo by adding a NaN z component.
/// Labels are not changed.
pub fn to_three(&self) -> CoordinatesThree {
match self {
Self::Label(s) => CoordinatesThree::Label(s.clone()),
Self::Coords(v) => CoordinatesThree::Coords([v[0], v[1], f32::NAN]),
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum CoordinatesThree {
Label(String),
Coords([Pfloat; 3]),
}
impl ToString for CoordinatesThree {
fn to_string(&self) -> String {
match self {
Self::Label(s) => s.to_owned(),
Self::Coords(v) => format!("{:?}", v),
}
}
}
impl CoordinatesThree {
/// Transform a CoordinatesThree into a CoordinatesTwo by deleting z component.
/// Labels are not changed.
pub fn to_two(&self) -> CoordinatesTwo {
match self {
Self::Label(s) => CoordinatesTwo::Label(s.clone()),
Self::Coords(v) => CoordinatesTwo::Coords([v[0], v[1]]),
}
}
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -73,20 +111,20 @@ pub struct System {
#[derive(Debug)] #[derive(Debug)]
pub struct Object { pub struct Object {
pub sprite: String, pub sprite: String,
pub position: Point2<f32>, pub position: Point3<f32>,
pub size: Pfloat, pub size: Pfloat,
pub parallax: Pfloat,
pub angle: Deg<Pfloat>, pub angle: Deg<Pfloat>,
} }
// Helper function for resolve_position, never called on its own.
fn resolve_coordinates( fn resolve_coordinates(
objects: &HashMap<String, toml::Object>, objects: &HashMap<String, toml::Object>,
cor: &toml::Coordinates, cor: &toml::CoordinatesThree,
mut cycle_detector: HashSet<String>, mut cycle_detector: HashSet<String>,
) -> Result<Point2<f32>> { ) -> Result<Point3<f32>> {
match cor { match cor {
toml::Coordinates::Coords(c) => Ok((*c).into()), toml::CoordinatesThree::Coords(c) => Ok((*c).into()),
toml::Coordinates::Label(l) => { toml::CoordinatesThree::Label(l) => {
if cycle_detector.contains(l) { if cycle_detector.contains(l) {
bail!( bail!(
"Found coordinate cycle: `{}`", "Found coordinate cycle: `{}`",
@ -111,19 +149,28 @@ fn resolve_coordinates(
} }
} }
/// Given an object, resolve it's position as a Point3.
fn resolve_position( fn resolve_position(
objects: &HashMap<String, toml::Object>, objects: &HashMap<String, toml::Object>,
obj: &toml::Object, obj: &toml::Object,
cycle_detector: HashSet<String>, cycle_detector: HashSet<String>,
) -> Result<Point2<f32>> { ) -> Result<Point3<f32>> {
match &obj.position { match &obj.position {
toml::Position::Cartesian(c) => Ok(resolve_coordinates(objects, c, cycle_detector)?), toml::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?),
toml::Position::Polar(p) => Ok(Polar { toml::Position::Polar(p) => {
center: resolve_coordinates(&objects, &p.center, cycle_detector)?, let r = resolve_coordinates(&objects, &p.center.to_three(), cycle_detector)?;
let plane = Polar {
center: (r.x, r.y).into(),
radius: p.radius, radius: p.radius,
angle: Deg(p.angle), angle: Deg(p.angle),
} }
.to_cartesian()), .to_cartesian();
Ok(Point3 {
x: plane.x,
y: plane.y,
z: p.z,
})
}
} }
} }
@ -140,7 +187,6 @@ impl System {
position: resolve_position(&value.object, &obj, cycle_detector) position: resolve_position(&value.object, &obj, cycle_detector)
.with_context(|| format!("In object {:#?}", label))?, .with_context(|| format!("In object {:#?}", label))?,
size: obj.size, size: obj.size,
parallax: obj.parallax,
angle: Deg(obj.angle.unwrap_or(0.0)), angle: Deg(obj.angle.unwrap_or(0.0)),
}); });
} }

View File

@ -1,11 +1,10 @@
use cgmath::{Deg, Point2}; use cgmath::{Deg, Point3};
use crate::{physics::Pfloat, render::Sprite, render::SpriteTexture, render::Spriteable}; use crate::{physics::Pfloat, render::Sprite, render::SpriteTexture, render::Spriteable};
pub struct Doodad { pub struct Doodad {
pub sprite: SpriteTexture, pub sprite: SpriteTexture,
pub pos: Point2<Pfloat>, pub pos: Point3<Pfloat>,
pub parallax: Pfloat,
pub size: Pfloat, pub size: Pfloat,
pub angle: Deg<Pfloat>, pub angle: Deg<Pfloat>,
} }
@ -18,7 +17,6 @@ impl Spriteable for Doodad {
pos: self.pos, pos: self.pos,
angle: self.angle, angle: self.angle,
size: self.size, size: self.size,
parallax: self.parallax,
}; };
} }
} }

View File

@ -75,9 +75,8 @@ impl Game {
// Make sure sprites are drawn in the correct order // Make sure sprites are drawn in the correct order
// (note the reversed a, b in the comparator) // (note the reversed a, b in the comparator)
// //
// TODO: use a gpu depth buffer with parallax as z-coordinate? // TODO: use a gpu depth buffer instead.
// Might be overkill. sprites.sort_by(|a, b| b.pos.z.total_cmp(&a.pos.z));
sprites.sort_by(|a, b| b.parallax.total_cmp(&a.parallax));
return sprites; return sprites;
} }

View File

@ -41,11 +41,10 @@ impl Ship {
impl Spriteable for Ship { impl Spriteable for Ship {
fn get_sprite(&self) -> Sprite { fn get_sprite(&self) -> Sprite {
return Sprite { return Sprite {
pos: self.body.pos, pos: (self.body.pos.x, self.body.pos.y, 1.0).into(),
texture: self.kind.sprite(), texture: self.kind.sprite(),
angle: self.body.angle, angle: self.body.angle,
scale: 1.0, scale: 1.0,
parallax: 1.0,
size: self.kind.size(), size: self.kind.size(),
}; };
} }

View File

@ -1,4 +1,4 @@
use cgmath::{Point2, Vector2}; use cgmath::{Point3, Vector2};
use rand::{self, Rng}; use rand::{self, Rng};
use super::Doodad; use super::Doodad;
@ -9,14 +9,14 @@ use crate::{
pub struct StarfieldStar { pub struct StarfieldStar {
/// Star coordinates, in world space. /// Star coordinates, in world space.
/// These are relative to the center of a starfield tile. /// These are relative to the center of a starfield tile.
pub pos: Point2<Pfloat>, pub pos: Point3<Pfloat>,
// TODO: z-coordinate? /// Height in game units.
pub parallax: Pfloat, /// Will be scaled for zoom, but not for distance.
pub size: Pfloat, pub size: Pfloat,
/// Color/brightness variation. /// Color/brightness variation. Random between 0 and 1.
/// See shader. /// Used in starfield shader.
pub tint: Vector2<Pfloat>, pub tint: Vector2<Pfloat>,
} }
@ -35,13 +35,12 @@ impl System {
bodies: Vec::new(), bodies: Vec::new(),
starfield: (0..consts::STARFIELD_COUNT) starfield: (0..consts::STARFIELD_COUNT)
.map(|_| StarfieldStar { .map(|_| StarfieldStar {
pos: Point2 { pos: Point3 {
x: rng.gen_range(-sz..=sz), x: rng.gen_range(-sz..=sz),
y: rng.gen_range(-sz..=sz), y: rng.gen_range(-sz..=sz),
z: rng.gen_range(consts::STARFIELD_Z_MIN..consts::STARFIELD_Z_MAX),
}, },
parallax: rng size: rng.gen_range(consts::STARFIELD_SIZE_MIN..consts::STARFIELD_SIZE_MAX),
.gen_range(consts::STARFIELD_PARALLAX_MIN..consts::STARFIELD_PARALLAX_MAX),
size: rng.gen_range(0.2..0.8), // TODO: configurable
tint: Vector2 { tint: Vector2 {
x: rng.gen_range(0.0..=1.0), x: rng.gen_range(0.0..=1.0),
y: rng.gen_range(0.0..=1.0), y: rng.gen_range(0.0..=1.0),
@ -55,7 +54,6 @@ impl System {
pos: o.position, pos: o.position,
sprite: SpriteTexture(o.sprite.to_owned()), sprite: SpriteTexture(o.sprite.to_owned()),
size: o.size, size: o.size,
parallax: o.parallax,
angle: o.angle, angle: o.angle,
}); });
} }

View File

@ -21,23 +21,29 @@ pub struct GlobalDataContent {
pub camera_position: [f32; 2], pub camera_position: [f32; 2],
/// Camera zoom value, in game units. /// Camera zoom value, in game units.
/// Only first component has meaning. /// Second component is ignored.
pub camera_zoom: [f32; 2], pub camera_zoom: [f32; 2],
/// Camera zoom min and max.
pub camera_zoom_limits: [f32; 2],
/// Size ratio of window, in physical pixels /// Size ratio of window, in physical pixels
pub window_size: [f32; 2], pub window_size: [f32; 2],
// Aspect ration of window // Aspect ration of window
/// Only first component has meaning. /// Second component is ignored.
pub window_aspect: [f32; 2], pub window_aspect: [f32; 2],
/// Texture index of starfield sprites /// Texture index of starfield sprites
/// Only first component has meaning. /// Second component is ignored.
pub starfield_texture: [u32; 2], pub starfield_texture: [u32; 2],
// Size of (square) starfield tiles, in game units // Size of (square) starfield tiles, in game units
/// Only first component has meaning. /// Second component is ignored.
pub starfield_tile_size: [f32; 2], pub starfield_tile_size: [f32; 2],
// Min and max starfield star size, in game units
pub starfield_size_limits: [f32; 2],
} }
impl GlobalDataContent { impl GlobalDataContent {

View File

@ -1,6 +1,6 @@
use anyhow::Result; use anyhow::Result;
use bytemuck; use bytemuck;
use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2}; use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2, Vector3};
use std::{iter, rc::Rc}; use std::{iter, rc::Rc};
use wgpu; use wgpu;
use winit::{self, dpi::PhysicalSize, window::Window}; use winit::{self, dpi::PhysicalSize, window::Window};
@ -16,7 +16,7 @@ use super::{
VertexBuffer, VertexBuffer,
}, },
}; };
use crate::{consts, game::Game}; use crate::{consts, game::Game, physics::Pfloat};
pub struct GPUState { pub struct GPUState {
device: wgpu::Device, device: wgpu::Device,
@ -209,14 +209,20 @@ impl GPUState {
let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom; let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom;
for s in game.get_sprites() { for s in game.get_sprites() {
// Parallax is computed here, so we can check if this sprite is visible. // Compute post-parallax position and distance-adjusted scale.
let pos = (s.pos - game.camera.pos.to_vec()) // We do this here so we can check if a sprite is on the screen.
/ (s.parallax + game.camera.zoom / consts::ZOOM_MIN); let pos: Point2<Pfloat> = {
(Point2 {
x: s.pos.x,
y: s.pos.y,
} - game.camera.pos.to_vec())
/ (s.pos.z + game.camera.zoom / consts::ZOOM_MIN)
};
let texture = self.texture_array.get_sprite_texture(s.texture); let texture = self.texture_array.get_sprite_texture(s.texture);
// Game dimensions of this sprite post-scale. // Game dimensions of this sprite post-scale.
// Don't divide by 2, we use this later. // Don't divide by 2, we use this later.
let height = s.size * s.scale / s.parallax; let height = s.size * s.scale / s.pos.z;
let width = height * texture.aspect; let width = height * texture.aspect;
// Don't draw (or compute matrices for) // Don't draw (or compute matrices for)
@ -233,7 +239,7 @@ impl GPUState {
position: pos.into(), position: pos.into(),
aspect: texture.aspect, aspect: texture.aspect,
rotation: Matrix2::from_angle(s.angle).into(), rotation: Matrix2::from_angle(s.angle).into(),
height, size: height,
texture_index: texture.index, texture_index: texture.index,
}) })
} }
@ -262,7 +268,7 @@ impl GPUState {
// Parallax correction. // Parallax correction.
// Also, adjust v for mod to work properly // Also, adjust v for mod to work properly
// (v is centered at 0) // (v is centered at 0)
let v: Point2<f32> = clip_nw * consts::STARFIELD_PARALLAX_MIN; let v: Point2<f32> = clip_nw * consts::STARFIELD_Z_MIN;
let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into(); let v_adj: Point2<f32> = (v.x + (sz / 2.0), v.y + (sz / 2.0)).into();
#[rustfmt::skip] #[rustfmt::skip]
@ -296,14 +302,14 @@ impl GPUState {
let mut instances = Vec::new(); let mut instances = Vec::new();
for x in (-nw_tile.x)..=nw_tile.x { for x in (-nw_tile.x)..=nw_tile.x {
for y in (-nw_tile.y)..=nw_tile.y { for y in (-nw_tile.y)..=nw_tile.y {
let offset = Vector2 { let offset = Vector3 {
x: sz * x as f32, x: sz * x as f32,
y: sz * y as f32, y: sz * y as f32,
z: 0.0,
}; };
for s in &game.system.starfield { for s in &game.system.starfield {
instances.push(StarfieldInstance { instances.push(StarfieldInstance {
position: (s.pos + offset).into(), position: (s.pos + offset).into(),
parallax: s.parallax,
size: s.size, size: s.size,
tint: s.tint.into(), tint: s.tint.into(),
}) })
@ -364,6 +370,7 @@ impl GPUState {
bytemuck::cast_slice(&[GlobalDataContent { bytemuck::cast_slice(&[GlobalDataContent {
camera_position: game.camera.pos.into(), camera_position: game.camera.pos.into(),
camera_zoom: [game.camera.zoom, 0.0], camera_zoom: [game.camera.zoom, 0.0],
camera_zoom_limits: [consts::ZOOM_MIN, consts::ZOOM_MAX],
window_size: [ window_size: [
self.window_size.width as f32, self.window_size.width as f32,
self.window_size.height as f32, self.window_size.height as f32,
@ -371,6 +378,7 @@ impl GPUState {
window_aspect: [self.window_aspect, 0.0], window_aspect: [self.window_aspect, 0.0],
starfield_texture: [self.texture_array.get_starfield_texture().index, 0], starfield_texture: [self.texture_array.get_starfield_texture().index, 0],
starfield_tile_size: [consts::STARFIELD_SIZE as f32, 0.0], starfield_tile_size: [consts::STARFIELD_SIZE as f32, 0.0],
starfield_size_limits: [consts::STARFIELD_SIZE_MIN, consts::STARFIELD_SIZE_MAX],
}]), }]),
); );

View File

@ -2,7 +2,7 @@ struct InstanceInput {
@location(2) rotation_matrix_0: vec2<f32>, @location(2) rotation_matrix_0: vec2<f32>,
@location(3) rotation_matrix_1: vec2<f32>, @location(3) rotation_matrix_1: vec2<f32>,
@location(4) position: vec2<f32>, @location(4) position: vec2<f32>,
@location(5) height: f32, @location(5) size: f32,
@location(6) aspect: f32, @location(6) aspect: f32,
@location(7) texture_idx: u32, @location(7) texture_idx: u32,
}; };
@ -24,10 +24,12 @@ var<uniform> global: GlobalUniform;
struct GlobalUniform { struct GlobalUniform {
camera_position: vec2<f32>, camera_position: vec2<f32>,
camera_zoom: vec2<f32>, camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>, window_size: vec2<f32>,
window_aspect: vec2<f32>, window_aspect: vec2<f32>,
starfield_texture: vec2<u32>, starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>, starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
}; };
@ -47,7 +49,7 @@ fn vertex_main(
// Apply sprite aspect ratio & scale factor // Apply sprite aspect ratio & scale factor
// This must be done *before* rotation. // This must be done *before* rotation.
let scale = instance.height / global.camera_zoom.x; let scale = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2<f32>( var pos: vec2<f32> = vec2<f32>(
vertex.position.x * instance.aspect * scale, vertex.position.x * instance.aspect * scale,
vertex.position.y * scale vertex.position.y * scale

View File

@ -1,8 +1,7 @@
struct InstanceInput { struct InstanceInput {
@location(2) position: vec2<f32>, @location(2) position: vec3<f32>,
@location(3) parallax: f32, @location(3) size: f32,
@location(4) size: f32, @location(4) tint: vec2<f32>,
@location(5) tint: vec2<f32>,
}; };
struct VertexInput { struct VertexInput {
@ -21,10 +20,12 @@ var<uniform> global: GlobalUniform;
struct GlobalUniform { struct GlobalUniform {
camera_position: vec2<f32>, camera_position: vec2<f32>,
camera_zoom: vec2<f32>, camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>, window_size: vec2<f32>,
window_aspect: vec2<f32>, window_aspect: vec2<f32>,
starfield_texture: vec2<u32>, starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>, starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
}; };
@ -62,19 +63,37 @@ fn vertex_main(
) )
); );
let zoom_min_times = (
global.camera_zoom.x / global.camera_zoom_limits.x
);
// Hide n% of the smallest stars
// If we wanted a constant number of stars on the screen, we would do
// `let hide_fraction = 1.0 - 1.0 / (zoom_min_times * zoom_min_times);`
// We, however, don't want this: a bigger screen should have more stars,
// but not *too* many. We thus scale linearly.
let hide_fraction = 1.0 - 1.0 / (zoom_min_times * 0.8);
// Hide some stars at large zoom levels.
if (
instance.size < (
hide_fraction * (global.starfield_size_limits.y - global.starfield_size_limits.x)
+ (global.starfield_size_limits.x)
)
) {
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out;
}
// Apply sprite aspect ratio & scale factor // Apply sprite aspect ratio & scale factor
// also applies screen aspect ratio // also applies screen aspect ratio
// Note that we do NOT scale for distance here---this is intentional.
var scale: f32 = instance.size / global.camera_zoom.x; var scale: f32 = instance.size / global.camera_zoom.x;
// Minimum scale to prevent flicker at large zoom levels // Minimum scale to prevent flicker at large zoom levels
var real_size = scale * global.window_size.xy; var real_size = scale * global.window_size.xy;
// TODO: configurable.
// Uniform distribution!
if (real_size.x < 0.5 || real_size.y < 0.5) {
// If this star is too small, don't even show it
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out;
}
if (real_size.x < 2.0 || real_size.y < 2.0) { if (real_size.x < 2.0 || real_size.y < 2.0) {
// Otherwise, clamp to a minimum scale // Otherwise, clamp to a minimum scale
scale = 2.0 / max(global.window_size.x, global.window_size.y); scale = 2.0 / max(global.window_size.x, global.window_size.y);
@ -89,16 +108,16 @@ fn vertex_main(
// World position relative to camera // World position relative to camera
// (Note that instance position is in a different // (Note that instance position is in a different
// coordinate system than usual) // coordinate system than usual)
let camera_pos = (instance.position + tile_center) - global.camera_position.xy; let camera_pos = (instance.position.xy + tile_center) - global.camera_position.xy;
// Translate // Translate
pos = pos + ( pos = pos + (
// Don't forget to correct distance for screen aspect ratio too! // Don't forget to correct distance for screen aspect ratio too!
(camera_pos / (global.camera_zoom.x * (instance.parallax))) (camera_pos / (global.camera_zoom.x * (instance.position.z)))
/ vec2<f32>(global.window_aspect.x, 1.0) / vec2<f32>(global.window_aspect.x, 1.0)
); );
out.position = vec4<f32>(pos, 0.0, 1.0) * instance.parallax; out.position = vec4<f32>(pos, 0.0, 1.0) * instance.position.z;
return out; return out;
} }

View File

@ -1,4 +1,4 @@
use cgmath::{Deg, Point2}; use cgmath::{Deg, Point3};
use super::SpriteTexture; use super::SpriteTexture;
use crate::physics::Pfloat; use crate::physics::Pfloat;
@ -8,7 +8,7 @@ pub struct Sprite {
pub texture: SpriteTexture, pub texture: SpriteTexture,
/// This object's position, in world coordinates. /// This object's position, in world coordinates.
pub pos: Point2<Pfloat>, pub pos: Point3<Pfloat>,
/// The size of this sprite, /// The size of this sprite,
/// given as height in world units. /// given as height in world units.
@ -21,11 +21,6 @@ pub struct Sprite {
/// This sprite's rotation /// This sprite's rotation
/// (relative to north, measured ccw) /// (relative to north, measured ccw)
pub angle: Deg<Pfloat>, pub angle: Deg<Pfloat>,
/// Parallax factor.
/// Corresponds to z-distance, and affects
/// position and scale.
pub parallax: Pfloat,
} }
pub trait Spriteable { pub trait Spriteable {

View File

@ -36,15 +36,20 @@ impl BufferObject for TexturedVertex {
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct StarfieldInstance { pub struct StarfieldInstance {
/// Position in origin field tile. /// Position in the starfield.
/// note that this is DIFFERENT from ///
/// the way we provide sprite positions! /// This is NOT world position, i.e, different from sprite positioning!
pub position: [f32; 2], /// The x and y coordinates here represent position relative to the center
/// of a starfield tile in world units, which is converted to world position
/// Parallax factor (same unit as usual) /// by the starfield vertex shader.
pub parallax: f32, pub position: [f32; 3],
/// Star size, in world units. This does NOT scale with distance,
/// unlike sprite size.
pub size: f32, pub size: f32,
/// Parameters for this star's color variation,
/// see the starfield fragment shader.
pub tint: [f32; 2], pub tint: [f32; 2],
} }
@ -58,24 +63,18 @@ impl BufferObject for StarfieldInstance {
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: 0, offset: 0,
shader_location: 2, shader_location: 2,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x3,
},
// Parallax
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
shader_location: 3,
format: wgpu::VertexFormat::Float32,
}, },
// Size // Size
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress,
shader_location: 4, shader_location: 3,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Tint // Tint
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 4]>() as wgpu::BufferAddress,
shader_location: 5, shader_location: 4,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x2,
}, },
], ],
@ -91,10 +90,13 @@ pub struct SpriteInstance {
pub rotation: [[f32; 2]; 2], pub rotation: [[f32; 2]; 2],
/// World position, relative to camera /// World position, relative to camera
/// Note that this does NOT contain z-distance,
/// since sprite parallax and distance scaling
/// is applied beforehand.
pub position: [f32; 2], pub position: [f32; 2],
/// Height of (unrotated) sprite in world units /// Height of (unrotated) sprite in world units
pub height: f32, pub size: f32,
// Sprite aspect ratio (width / height) // Sprite aspect ratio (width / height)
pub aspect: f32, pub aspect: f32,
@ -129,7 +131,7 @@ impl BufferObject for SpriteInstance {
shader_location: 4, shader_location: 4,
format: wgpu::VertexFormat::Float32x2, format: wgpu::VertexFormat::Float32x2,
}, },
// Height // Size
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress,
shader_location: 5, shader_location: 5,