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]
sprite = "star::star"
position = [0.0, 0.0]
position = [0.0, 0.0, 20.0]
size = 1000
parallax = 20.0
[object.earth]
@ -14,8 +13,8 @@ sprite = "planet::earth"
position.center = "star"
position.radius = 4000
position.angle = 0
position.z = 10.0
size = 1000
parallax = 10.0
[object.luna]
@ -23,6 +22,6 @@ sprite = "planet::luna"
position.center = "earth"
position.radius = 1600
position.angle = 135
position.z = 7.8
size = 500
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_MAX: Pfloat = 2000.0;
// Z-axis range for starfield stars
pub const STARFIELD_PARALLAX_MIN: f32 = 100.0;
pub const STARFIELD_PARALLAX_MAX: f32 = 200.0;
// Size of a square starfield tile, in game units.
// A tile of size PARALLAX_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_PARALLAX_MAX as u64 * ZOOM_MAX as u64;
// Average number of stars per game unit
/// 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
/// 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;
/// Root directory of game content
pub const CONTENT_ROOT: &'static str = "./content";

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
use anyhow::Result;
use bytemuck;
use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2};
use cgmath::{EuclideanSpace, Matrix2, Point2, Vector2, Vector3};
use std::{iter, rc::Rc};
use wgpu;
use winit::{self, dpi::PhysicalSize, window::Window};
@ -16,7 +16,7 @@ use super::{
VertexBuffer,
},
};
use crate::{consts, game::Game};
use crate::{consts, game::Game, physics::Pfloat};
pub struct GPUState {
device: wgpu::Device,
@ -209,14 +209,20 @@ impl GPUState {
let clip_sw = Point2::from((self.window_aspect, -1.0)) * game.camera.zoom;
for s in game.get_sprites() {
// Parallax is computed here, so we can check if this sprite is visible.
let pos = (s.pos - game.camera.pos.to_vec())
/ (s.parallax + game.camera.zoom / consts::ZOOM_MIN);
// Compute post-parallax position and distance-adjusted scale.
// We do this here so we can check if a sprite is on the screen.
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);
// Game dimensions of this sprite post-scale.
// 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;
// Don't draw (or compute matrices for)
@ -233,7 +239,7 @@ impl GPUState {
position: pos.into(),
aspect: texture.aspect,
rotation: Matrix2::from_angle(s.angle).into(),
height,
size: height,
texture_index: texture.index,
})
}
@ -262,7 +268,7 @@ impl GPUState {
// Parallax correction.
// Also, adjust v for mod to work properly
// (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();
#[rustfmt::skip]
@ -296,14 +302,14 @@ impl GPUState {
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 {
let offset = Vector3 {
x: sz * x as f32,
y: sz * y as f32,
z: 0.0,
};
for s in &game.system.starfield {
instances.push(StarfieldInstance {
position: (s.pos + offset).into(),
parallax: s.parallax,
size: s.size,
tint: s.tint.into(),
})
@ -364,6 +370,7 @@ impl GPUState {
bytemuck::cast_slice(&[GlobalDataContent {
camera_position: game.camera.pos.into(),
camera_zoom: [game.camera.zoom, 0.0],
camera_zoom_limits: [consts::ZOOM_MIN, consts::ZOOM_MAX],
window_size: [
self.window_size.width as f32,
self.window_size.height as f32,
@ -371,6 +378,7 @@ impl GPUState {
window_aspect: [self.window_aspect, 0.0],
starfield_texture: [self.texture_array.get_starfield_texture().index, 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(3) rotation_matrix_1: vec2<f32>,
@location(4) position: vec2<f32>,
@location(5) height: f32,
@location(5) size: f32,
@location(6) aspect: f32,
@location(7) texture_idx: u32,
};
@ -24,10 +24,12 @@ var<uniform> global: GlobalUniform;
struct GlobalUniform {
camera_position: vec2<f32>,
camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>,
window_aspect: vec2<f32>,
starfield_texture: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
};
@ -47,7 +49,7 @@ fn vertex_main(
// Apply sprite aspect ratio & scale factor
// 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>(
vertex.position.x * instance.aspect * scale,
vertex.position.y * scale

View File

@ -1,8 +1,7 @@
struct InstanceInput {
@location(2) position: vec2<f32>,
@location(3) parallax: f32,
@location(4) size: f32,
@location(5) tint: vec2<f32>,
@location(2) position: vec3<f32>,
@location(3) size: f32,
@location(4) tint: vec2<f32>,
};
struct VertexInput {
@ -21,10 +20,12 @@ var<uniform> global: GlobalUniform;
struct GlobalUniform {
camera_position: vec2<f32>,
camera_zoom: vec2<f32>,
camera_zoom_limits: vec2<f32>,
window_size: vec2<f32>,
window_aspect: vec2<f32>,
starfield_texture: vec2<u32>,
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
// 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;
// Minimum scale to prevent flicker at large zoom levels
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) {
// Otherwise, clamp to a minimum scale
scale = 2.0 / max(global.window_size.x, global.window_size.y);
@ -89,16 +108,16 @@ fn vertex_main(
// World position relative to camera
// (Note that instance position is in a different
// 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
pos = pos + (
// 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)
);
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;
}

View File

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

View File

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