Compare commits

...

7 Commits

Author SHA1 Message Date
Mark a4ca62e1dc
Added sprite data uniform 2024-01-04 18:15:30 -08:00
Mark 613245b92e
typo 2024-01-04 17:50:52 -08:00
Mark 37a0aca5af
Updated TODO 2024-01-04 17:20:41 -08:00
Mark f926f5f172
Asset update 2024-01-04 17:18:39 -08:00
Mark 7cde4f2346
Adapted renderer for new texture indexing 2024-01-04 17:18:31 -08:00
Mark c382431747
Renamed content fields,
reworked texture indexing
2024-01-04 17:17:55 -08:00
Mark 10f9776108
Improved image packer 2024-01-04 17:15:32 -08:00
47 changed files with 953 additions and 639 deletions

View File

@ -100,6 +100,7 @@
- Conversations - Conversations
- Trade - Trade
- Missions - Missions
- Procedural suns
## Camera ## Camera
- Shake/wobble on heavy hits? - Shake/wobble on heavy hits?
@ -134,6 +135,8 @@
- Handles - Handles
- Content specification and pipeline - Content specification and pipeline
- How packer and optimizations work, and why - How packer and optimizations work, and why
- How big should sprites be? (resize existing)
- Naming: atlas, sprite, image, frame, texture
## Ideas ## Ideas

2
assets

@ -1 +1 @@
Subproject commit 6e4b7022a963a3357ba5f578bdd91064ae9f6b3e Subproject commit 05e5272f3789b44192d0bf2bd253641e76659a2a

View File

@ -7,8 +7,8 @@ rate = 0.2
# Random rate variation (each cooldown is +- this) # Random rate variation (each cooldown is +- this)
rate_rng = 0.1 rate_rng = 0.1
# TODO: apply force on fire # TODO: apply force to ship on fire
projectile.sprite_texture = "projectile::blaster" projectile.sprite = "projectile::blaster"
# Height of projectile in game units # Height of projectile in game units
projectile.size = 6 projectile.size = 6
projectile.size_rng = 0.0 projectile.size_rng = 0.0
@ -29,13 +29,13 @@ projectile.force = 0.0
projectile.collider.ball.radius = 2.0 projectile.collider.ball.radius = 2.0
projectile.impact.texture = "particle::blaster" projectile.impact.sprite = "particle::explosion"
projectile.impact.lifetime = "inherit" projectile.impact.lifetime = "inherit"
projectile.impact.inherit_velocity = "target" projectile.impact.inherit_velocity = "target"
projectile.impact.size = 3.0 projectile.impact.size = 3.0
projectile.expire.texture = "particle::blaster" projectile.expire.sprite = "particle::blaster"
projectile.expire.lifetime = "inherit" projectile.expire.lifetime = "inherit"
projectile.expire.inherit_velocity = "projectile" projectile.expire.inherit_velocity = "projectile"
projectile.expire.size = 3.0 projectile.expire.size = 3.0

View File

@ -3,5 +3,5 @@
space.engine = 20 space.engine = 20
engine.thrust = 100 engine.thrust = 100
engine.flare_texture = "flare::ion" engine.flare_sprite = "flare::ion"
steering.power = 20 steering.power = 20

View File

@ -1,5 +1,5 @@
[ship."Gypsum"] [ship."Gypsum"]
sprite_texture = "ship::gypsum" sprite = "ship::gypsum"
size = 100 size = 100
mass = 1 mass = 1
hull = 200 hull = 200

View File

@ -1,40 +1,40 @@
[texture."starfield"] [sprite."starfield"]
file = "starfield.png" file = "starfield.png"
[texture."star::star"] [sprite."star::star"]
file = "star/B-09.png" file = "star/B-09.png"
[texture."flare::ion"] [sprite."flare::ion"]
file = "flare/1.png" file = "flare/1.png"
[texture."planet::earth"] [sprite."planet::earth"]
file = "planet/earth.png" file = "planet/earth.png"
[texture."planet::luna"] [sprite."planet::luna"]
file = "planet/luna.png" file = "planet/luna.png"
[texture."projectile::blaster"] [sprite."projectile::blaster"]
file = "projectile/blaster.png" file = "projectile/blaster.png"
[texture."ship::gypsum"] [sprite."ship::gypsum"]
file = "ship/gypsum.png" file = "ship/gypsum.png"
[texture."ui::radar"] [sprite."ui::radar"]
file = "ui/radar.png" file = "ui/radar.png"
[texture."ui::shipblip"] [sprite."ui::shipblip"]
file = "ui/ship-blip.png" file = "ui/ship-blip.png"
[texture."ui::planetblip"] [sprite."ui::planetblip"]
file = "ui/planet-blip.png" file = "ui/planet-blip.png"
[texture."ui::radarframe"] [sprite."ui::radarframe"]
file = "ui/radarframe.png" file = "ui/radarframe.png"
[texture."ui::centerarrow"] [sprite."ui::centerarrow"]
file = "ui/center-arrow.png" file = "ui/center-arrow.png"
[texture."particle::blaster"] [sprite."particle::blaster"]
duration = 0.15 duration = 0.15
repeat = "once" repeat = "once"
frames = [ frames = [
@ -45,7 +45,7 @@ frames = [
] ]
[texture."particle::explosion"] [sprite."particle::explosion"]
duration = 0.4 duration = 0.4
repeat = "once" repeat = "once"
frames = [ frames = [

View File

@ -1,17 +1,17 @@
[system."12 Autumn Above"] [system."12 Autumn Above"]
object.star.sprite_texture = "star::star" object.star.sprite = "star::star"
object.star.position = [0.0, 0.0, 30.0] object.star.position = [0.0, 0.0, 30.0]
object.star.size = 2000 object.star.size = 2000
object.earth.sprite_texture = "planet::earth" object.earth.sprite = "planet::earth"
object.earth.position.center = "star" object.earth.position.center = "star"
object.earth.position.radius = 4000 object.earth.position.radius = 4000
object.earth.position.angle = 0 object.earth.position.angle = 0
object.earth.position.z = 10.0 object.earth.position.z = 10.0
object.earth.size = 1000 object.earth.size = 1000
object.luna.sprite_texture = "planet::luna" object.luna.sprite = "planet::luna"
object.luna.position.center = "earth" object.luna.position.center = "earth"
object.luna.position.radius = 1600 object.luna.position.radius = 1600
object.luna.position.angle = 135 object.luna.position.angle = 135

View File

@ -35,14 +35,14 @@ pub const STARFIELD_DENSITY: f64 = 0.01;
/// Must fit inside an i32 /// 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;
/// Name of starfield texture /// Name of starfield sprite
pub const STARFIELD_TEXTURE_NAME: &'static str = "starfield"; pub const STARFIELD_SPRITE_NAME: &'static str = "starfield";
/// Root directory of game content /// Root directory of game content
pub const CONTENT_ROOT: &'static str = "./content"; pub const CONTENT_ROOT: &'static str = "./content";
/// Root directory of game textures /// Root directory of game images
pub const TEXTURE_ROOT: &'static str = "./assets/render"; pub const IMAGE_ROOT: &'static str = "./assets/render";
/// We can draw at most this many object sprites on the screen. /// We can draw at most this many object sprites on the screen.
pub const OBJECT_SPRITE_INSTANCE_LIMIT: u64 = 500; pub const OBJECT_SPRITE_INSTANCE_LIMIT: u64 = 500;

View File

@ -17,6 +17,8 @@ readme = { workspace = true }
workspace = true workspace = true
[dependencies] [dependencies]
galactica-packer = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }

View File

@ -7,24 +7,28 @@
//! in our code. It's managable, but the approach here is simpler and easier to understand. //! in our code. It's managable, but the approach here is simpler and easier to understand.
use std::{cmp::Eq, hash::Hash}; use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a /// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct TextureHandle { pub struct SpriteHandle {
/// The index of this texture in content.textures /// The index of this sprite in content.sprites
pub(crate) index: usize, /// This must be public, since render uses this to
/// select sprites.
///
/// This is a u32 for that same reason, too.
pub index: u32,
/// The aspect ratio of this texture (width / height) /// The aspect ratio of this sprite (width / height)
pub aspect: f32, pub aspect: f32,
} }
impl Hash for TextureHandle { impl Hash for SpriteHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) { fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state) self.index.hash(state)
} }
} }
impl Eq for TextureHandle {} impl Eq for SpriteHandle {}
impl PartialEq for TextureHandle { impl PartialEq for SpriteHandle {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.index.eq(&other.index) self.index.eq(&other.index)
} }

View File

@ -8,6 +8,7 @@ mod part;
mod util; mod util;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use std::{ use std::{
collections::HashMap, collections::HashMap,
fs::File, fs::File,
@ -17,10 +18,10 @@ use std::{
use toml; use toml;
use walkdir::WalkDir; use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle}; pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle};
pub use part::{ pub use part::{
EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile, EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile,
ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, System, Texture, ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, Sprite, System,
}; };
mod syntax { mod syntax {
@ -28,7 +29,7 @@ mod syntax {
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, fmt::Display, hash::Hash}; use std::{collections::HashMap, fmt::Display, hash::Hash};
use crate::part::{faction, gun, outfit, ship, system, texture}; use crate::part::{faction, gun, outfit, ship, sprite, system};
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Root { pub struct Root {
@ -36,7 +37,7 @@ mod syntax {
pub ship: Option<HashMap<String, ship::syntax::Ship>>, pub ship: Option<HashMap<String, ship::syntax::Ship>>,
pub system: Option<HashMap<String, system::syntax::System>>, pub system: Option<HashMap<String, system::syntax::System>>,
pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>, pub outfit: Option<HashMap<String, outfit::syntax::Outfit>>,
pub texture: Option<HashMap<String, texture::syntax::Texture>>, pub sprite: Option<HashMap<String, sprite::syntax::Sprite>>,
pub faction: Option<HashMap<String, faction::syntax::Faction>>, pub faction: Option<HashMap<String, faction::syntax::Faction>>,
} }
@ -72,7 +73,7 @@ mod syntax {
ship: None, ship: None,
system: None, system: None,
outfit: None, outfit: None,
texture: None, sprite: None,
faction: None, faction: None,
} }
} }
@ -84,8 +85,8 @@ mod syntax {
.with_context(|| "while merging systems")?; .with_context(|| "while merging systems")?;
merge_hashmap(&mut self.outfit, other.outfit) merge_hashmap(&mut self.outfit, other.outfit)
.with_context(|| "while merging outfits")?; .with_context(|| "while merging outfits")?;
merge_hashmap(&mut self.texture, other.texture) merge_hashmap(&mut self.sprite, other.sprite)
.with_context(|| "while merging textures")?; .with_context(|| "while merging sprites")?;
merge_hashmap(&mut self.faction, other.faction) merge_hashmap(&mut self.faction, other.faction)
.with_context(|| "while merging factions")?; .with_context(|| "while merging factions")?;
return Ok(()); return Ok(());
@ -94,10 +95,10 @@ mod syntax {
} }
trait Build { trait Build {
type InputSyntax; type InputSyntaxType;
/// Build a processed System struct from raw serde data /// Build a processed System struct from raw serde data
fn build(root: Self::InputSyntax, ct: &mut Content) -> Result<()> fn build(root: Self::InputSyntaxType, ct: &mut Content) -> Result<()>
where where
Self: Sized; Self: Sized;
} }
@ -106,18 +107,21 @@ trait Build {
#[derive(Debug)] #[derive(Debug)]
pub struct Content { pub struct Content {
/* Configuration values */ /* Configuration values */
/// Root directory for textures /// Root directory for image
texture_root: PathBuf, image_root: PathBuf,
/// Name of starfield texture /// Name of starfield sprite
starfield_texture_name: String, starfield_sprite_name: String,
/// Textures /// Sprites
pub textures: Vec<part::texture::Texture>, pub sprites: Vec<part::sprite::Sprite>,
/// Map strings to texture names. /// Map strings to texture names.
/// This is only necessary because we need to hard-code a few texture names for UI elements. /// This is only necessary because we need to hard-code a few texture names for UI elements.
texture_index: HashMap<String, handle::TextureHandle>, sprite_index: HashMap<String, handle::SpriteHandle>,
/// The texture to use for starfield stars /// The texture to use for starfield stars
starfield_handle: Option<handle::TextureHandle>, starfield_handle: Option<handle::SpriteHandle>,
/// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas,
/// Outfits /// Outfits
outfits: Vec<part::outfit::Outfit>, outfits: Vec<part::outfit::Outfit>,
@ -148,6 +152,7 @@ impl Content {
pub fn load_dir( pub fn load_dir(
path: PathBuf, path: PathBuf,
texture_root: PathBuf, texture_root: PathBuf,
atlas_index: PathBuf,
starfield_texture_name: String, starfield_texture_name: String,
) -> Result<Self> { ) -> Result<Self> {
let mut root = syntax::Root::new(); let mut root = syntax::Root::new();
@ -177,23 +182,32 @@ impl Content {
} }
} }
let atlas: SpriteAtlas = {
let mut file_string = String::new();
let _ = File::open(atlas_index)?.read_to_string(&mut file_string);
let file_string = file_string.trim();
toml::from_str(&file_string)?
};
let mut content = Self { let mut content = Self {
sprite_atlas: atlas,
systems: Vec::new(), systems: Vec::new(),
ships: Vec::new(), ships: Vec::new(),
guns: Vec::new(), guns: Vec::new(),
outfits: Vec::new(), outfits: Vec::new(),
textures: Vec::new(), sprites: Vec::new(),
factions: Vec::new(), factions: Vec::new(),
texture_index: HashMap::new(), sprite_index: HashMap::new(),
starfield_handle: None, starfield_handle: None,
texture_root, image_root: texture_root,
starfield_texture_name, starfield_sprite_name: starfield_texture_name,
}; };
// Order here matters, usually // Order here matters, usually
if root.texture.is_some() { if root.sprite.is_some() {
part::texture::Texture::build(root.texture.take().unwrap(), &mut content)?; part::sprite::Sprite::build(root.sprite.take().unwrap(), &mut content)?;
} }
if root.ship.is_some() { if root.ship.is_some() {
part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?; part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?;
} }
@ -216,27 +230,32 @@ impl Content {
// Access methods // Access methods
impl Content { impl Content {
/// Get the texture handle for the starfield texture /// Get the handle for the starfield sprite
pub fn get_starfield_handle(&self) -> TextureHandle { pub fn get_starfield_handle(&self) -> SpriteHandle {
match self.starfield_handle { match self.starfield_handle {
Some(h) => h, Some(h) => h,
None => unreachable!("Starfield texture hasn't been loaded yet!"), None => unreachable!("Starfield sprite hasn't been loaded yet!"),
} }
} }
/// Get a handle from a texture name /// Get a handle from a sprite name
pub fn get_texture_handle(&self, name: &str) -> TextureHandle { pub fn get_sprite_handle(&self, name: &str) -> SpriteHandle {
return match self.texture_index.get(name) { return match self.sprite_index.get(name) {
Some(s) => *s, Some(s) => *s,
None => unreachable!("get_texture_handle was called with a bad handle!"), None => unreachable!("get_sprite_handle was called with a bad name!"),
}; };
} }
/// Get a texture from a handle /// Get a sprite from a handle
pub fn get_texture(&self, h: TextureHandle) -> &Texture { pub fn get_sprite(&self, h: SpriteHandle) -> &Sprite {
// In theory, this could fail if h has a bad index, but that shouldn't ever happen. // In theory, this could fail if h has a bad index, but that shouldn't ever happen.
// The only TextureHandles that exist should be created by this crate. // The only handles that exist should be created by this crate.
return &self.textures[h.index]; return &self.sprites[h.index as usize];
}
/// Get a sprite from a path
pub fn get_image(&self, p: &Path) -> &SpriteAtlasImage {
self.sprite_atlas.index.get(p).unwrap()
} }
/// Get an outfit from a handle /// Get an outfit from a handle
@ -249,7 +268,7 @@ impl Content {
return &self.guns[h.index]; return &self.guns[h.index];
} }
/// Get a texture from a handle /// Get a ship from a handle
pub fn get_ship(&self, h: ShipHandle) -> &Ship { pub fn get_ship(&self, h: ShipHandle) -> &Ship {
return &self.ships[h.index]; return &self.ships[h.index];
} }

View File

@ -59,9 +59,9 @@ pub struct Faction {
} }
impl crate::Build for Faction { impl crate::Build for Faction {
type InputSyntax = HashMap<String, syntax::Faction>; type InputSyntaxType = HashMap<String, syntax::Faction>;
fn build(factions: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(factions: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
// Keeps track of position in faction array. // Keeps track of position in faction array.
// This lets us build FactionHandles before finishing all factions. // This lets us build FactionHandles before finishing all factions.
let faction_names: Vec<String> = factions.keys().map(|x| x.to_owned()).collect(); let faction_names: Vec<String> = factions.keys().map(|x| x.to_owned()).collect();

View File

@ -3,7 +3,7 @@ use cgmath::Deg;
use serde::Deserialize; use serde::Deserialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{handle::TextureHandle, Content}; use crate::{handle::SpriteHandle, Content};
use crate::OutfitSpace; use crate::OutfitSpace;
@ -23,7 +23,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Projectile { pub struct Projectile {
pub sprite_texture: String, pub sprite: String,
pub size: f32, pub size: f32,
pub size_rng: f32, pub size_rng: f32,
pub speed: f32, pub speed: f32,
@ -40,7 +40,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct ProjectileParticle { pub struct ProjectileParticle {
pub texture: String, pub sprite: String,
pub lifetime: ParticleLifetime, pub lifetime: ParticleLifetime,
pub inherit_velocity: super::ImpactInheritVelocity, pub inherit_velocity: super::ImpactInheritVelocity,
pub size: f32, pub size: f32,
@ -110,7 +110,7 @@ pub struct Gun {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Projectile { pub struct Projectile {
/// The projectile sprite /// The projectile sprite
pub sprite_texture: TextureHandle, pub sprite: SpriteHandle,
/// The average size of this projectile /// The average size of this projectile
/// (height in game units) /// (height in game units)
@ -155,9 +155,9 @@ pub struct Projectile {
/// The particle a projectile will spawn when it hits something /// The particle a projectile will spawn when it hits something
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ProjectileParticle { pub struct ProjectileParticle {
/// The texture to use for this particle. /// The sprite to use for this particle.
/// This is most likely animated. /// This is most likely animated.
pub texture: TextureHandle, pub sprite: SpriteHandle,
/// How many seconds this particle should live /// How many seconds this particle should live
pub lifetime: f32, pub lifetime: f32,
@ -174,8 +174,8 @@ fn parse_projectile_particle(
p: Option<syntax::ProjectileParticle>, p: Option<syntax::ProjectileParticle>,
) -> Result<Option<ProjectileParticle>> { ) -> Result<Option<ProjectileParticle>> {
if let Some(impact) = p { if let Some(impact) = p {
let impact_texture = match ct.texture_index.get(&impact.texture) { let impact_sprite_handle = match ct.sprite_index.get(&impact.sprite) {
None => bail!("impact texture `{}` doesn't exist", impact.texture), None => bail!("impact sprite `{}` doesn't exist", impact.sprite),
Some(t) => *t, Some(t) => *t,
}; };
@ -183,8 +183,8 @@ fn parse_projectile_particle(
syntax::ParticleLifetime::Seconds(s) => s, syntax::ParticleLifetime::Seconds(s) => s,
syntax::ParticleLifetime::Inherit(s) => { syntax::ParticleLifetime::Inherit(s) => {
if s == "inherit" { if s == "inherit" {
let t = ct.get_texture(impact_texture); let sprite = ct.get_sprite(impact_sprite_handle);
t.fps * t.frames.len() as f32 sprite.fps * sprite.frames.len() as f32
} else { } else {
bail!("bad impact lifetime, must be float or \"inherit\"",) bail!("bad impact lifetime, must be float or \"inherit\"",)
} }
@ -192,7 +192,7 @@ fn parse_projectile_particle(
}; };
Ok(Some(ProjectileParticle { Ok(Some(ProjectileParticle {
texture: impact_texture, sprite: impact_sprite_handle,
lifetime: impact_lifetime, lifetime: impact_lifetime,
inherit_velocity: impact.inherit_velocity, inherit_velocity: impact.inherit_velocity,
size: impact.size, size: impact.size,
@ -203,15 +203,15 @@ fn parse_projectile_particle(
} }
impl crate::Build for Gun { impl crate::Build for Gun {
type InputSyntax = HashMap<String, syntax::Gun>; type InputSyntaxType = HashMap<String, syntax::Gun>;
fn build(gun: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(gun: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (gun_name, gun) in gun { for (gun_name, gun) in gun {
let projectile_texture = match ct.texture_index.get(&gun.projectile.sprite_texture) { let projectile_sprite_handle = match ct.sprite_index.get(&gun.projectile.sprite) {
None => bail!( None => bail!(
"In gun `{}`: projectile texture `{}` doesn't exist", "In gun `{}`: projectile sprite `{}` doesn't exist",
gun_name, gun_name,
gun.projectile.sprite_texture gun.projectile.sprite
), ),
Some(t) => *t, Some(t) => *t,
}; };
@ -229,7 +229,7 @@ impl crate::Build for Gun {
rate_rng: gun.rate_rng, rate_rng: gun.rate_rng,
projectile: Projectile { projectile: Projectile {
force: gun.projectile.force, force: gun.projectile.force,
sprite_texture: projectile_texture, sprite: projectile_sprite_handle,
size: gun.projectile.size, size: gun.projectile.size,
size_rng: gun.projectile.size_rng, size_rng: gun.projectile.size_rng,
speed: gun.projectile.speed, speed: gun.projectile.speed,

View File

@ -5,13 +5,13 @@ pub mod gun;
pub mod outfit; pub mod outfit;
mod shared; mod shared;
pub mod ship; pub mod ship;
pub mod sprite;
pub mod system; pub mod system;
pub mod texture;
pub use faction::{Faction, Relationship}; pub use faction::{Faction, Relationship};
pub use gun::{Gun, ImpactInheritVelocity, Projectile, ProjectileCollider, ProjectileParticle}; pub use gun::{Gun, ImpactInheritVelocity, Projectile, ProjectileCollider, ProjectileParticle};
pub use outfit::Outfit; pub use outfit::Outfit;
pub use shared::OutfitSpace; pub use shared::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship}; pub use ship::{EnginePoint, GunPoint, Ship};
pub use sprite::{RepeatMode, Sprite};
pub use system::{Object, System}; pub use system::{Object, System};
pub use texture::{RepeatMode, Texture};

View File

@ -2,7 +2,7 @@ use std::collections::HashMap;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use crate::{handle::TextureHandle, Content, OutfitSpace}; use crate::{handle::SpriteHandle, Content, OutfitSpace};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::shared; use crate::part::shared;
@ -20,7 +20,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Engine { pub struct Engine {
pub thrust: f32, pub thrust: f32,
pub flare_texture: String, pub flare_sprite: String,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -44,37 +44,37 @@ pub struct Outfit {
/// The engine flare sprite this outfit creates. /// The engine flare sprite this outfit creates.
/// Its location and size is determined by a ship's /// Its location and size is determined by a ship's
/// engine points. /// engine points.
pub engine_flare_texture: Option<TextureHandle>, pub engine_flare_sprite: Option<SpriteHandle>,
/// How much space this outfit requires /// How much space this outfit requires
pub space: OutfitSpace, pub space: OutfitSpace,
} }
impl crate::Build for Outfit { impl crate::Build for Outfit {
type InputSyntax = HashMap<String, syntax::Outfit>; type InputSyntaxType = HashMap<String, syntax::Outfit>;
fn build(outfits: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(outfits: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (outfit_name, outfit) in outfits { for (outfit_name, outfit) in outfits {
let mut o = Self { let mut o = Self {
name: outfit_name.clone(), name: outfit_name.clone(),
engine_thrust: 0.0, engine_thrust: 0.0,
steer_power: 0.0, steer_power: 0.0,
engine_flare_texture: None, engine_flare_sprite: None,
space: OutfitSpace::from(outfit.space), space: OutfitSpace::from(outfit.space),
}; };
// Engine stats // Engine stats
if let Some(engine) = outfit.engine { if let Some(engine) = outfit.engine {
let th = match ct.texture_index.get(&engine.flare_texture) { let th = match ct.sprite_index.get(&engine.flare_sprite) {
None => bail!( None => bail!(
"In outfit `{}`: texture `{}` doesn't exist", "In outfit `{}`: flare sprite `{}` doesn't exist",
outfit_name, outfit_name,
engine.flare_texture engine.flare_sprite
), ),
Some(t) => *t, Some(t) => *t,
}; };
o.engine_thrust = engine.thrust; o.engine_thrust = engine.thrust;
o.engine_flare_texture = Some(th); o.engine_flare_sprite = Some(th);
} }
// Steering stats // Steering stats

View File

@ -4,7 +4,7 @@ use anyhow::{bail, Result};
use cgmath::Point2; use cgmath::Point2;
use nalgebra::{point, Point}; use nalgebra::{point, Point};
use crate::{handle::TextureHandle, Content, OutfitSpace}; use crate::{handle::SpriteHandle, Content, OutfitSpace};
pub(crate) mod syntax { pub(crate) mod syntax {
use crate::part::shared; use crate::part::shared;
@ -15,7 +15,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Ship { pub struct Ship {
pub sprite_texture: String, pub sprite: String,
pub size: f32, pub size: f32,
pub engines: Vec<Engine>, pub engines: Vec<Engine>,
pub guns: Vec<Gun>, pub guns: Vec<Gun>,
@ -57,7 +57,7 @@ pub struct Ship {
pub name: String, pub name: String,
/// This ship's sprite /// This ship's sprite
pub sprite_texture: TextureHandle, pub sprite: SpriteHandle,
/// The size of this ship. /// The size of this ship.
/// Measured as unrotated height, /// Measured as unrotated height,
@ -123,26 +123,26 @@ pub struct GunPoint {
} }
impl crate::Build for Ship { impl crate::Build for Ship {
type InputSyntax = HashMap<String, syntax::Ship>; type InputSyntaxType = HashMap<String, syntax::Ship>;
fn build(ship: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(ship: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (ship_name, ship) in ship { for (ship_name, ship) in ship {
let th = match ct.texture_index.get(&ship.sprite_texture) { let handle = match ct.sprite_index.get(&ship.sprite) {
None => bail!( None => bail!(
"In ship `{}`: texture `{}` doesn't exist", "In ship `{}`: sprite `{}` doesn't exist",
ship_name, ship_name,
ship.sprite_texture ship.sprite
), ),
Some(t) => *t, Some(t) => *t,
}; };
let size = ship.size; let size = ship.size;
let aspect = th.aspect; let aspect = ct.get_sprite(handle).aspect;
ct.ships.push(Self { ct.ships.push(Self {
aspect, aspect,
name: ship_name, name: ship_name,
sprite_texture: th, sprite: handle,
mass: ship.mass, mass: ship.mass,
space: OutfitSpace::from(ship.space), space: OutfitSpace::from(ship.space),
angular_drag: ship.angular_drag, angular_drag: ship.angular_drag,

View File

@ -3,7 +3,7 @@ use image::io::Reader;
use serde::Deserialize; use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};
use crate::{handle::TextureHandle, Content}; use crate::{handle::SpriteHandle, Content};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize; use serde::Deserialize;
@ -16,18 +16,18 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
#[serde(untagged)] #[serde(untagged)]
pub enum Texture { pub enum Sprite {
Static(StaticTexture), Static(StaticSprite),
Frames(FramesTexture), Frames(FrameSprite),
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct StaticTexture { pub struct StaticSprite {
pub file: PathBuf, pub file: PathBuf,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct FramesTexture { pub struct FrameSprite {
pub frames: Vec<PathBuf>, pub frames: Vec<PathBuf>,
pub duration: f32, pub duration: f32,
pub repeat: RepeatMode, pub repeat: RepeatMode,
@ -57,90 +57,94 @@ impl RepeatMode {
} }
} }
/// Represents a texture that may be used in the game. /// Represents a sprite that may be used in the game.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Texture { pub struct Sprite {
/// The name of this texture /// The name of this sprite
pub name: String, pub name: String,
/// The handle for this texture /// This sprite's handle
pub handle: TextureHandle, pub handle: SpriteHandle,
/// The frames of this texture /// The file names of frames of this sprite.
/// (static textures have one frame) /// unanimated sprites have one frame.
pub frames: Vec<PathBuf>, pub frames: Vec<PathBuf>,
/// The speed of this texture's animation /// The speed of this sprite's animation.
/// (static textures have zero fps) /// unanimated sprites have zero fps.
pub fps: f32, pub fps: f32,
/// How to replay this texture's animation /// How to replay this sprite's animation
pub repeat: RepeatMode, pub repeat: RepeatMode,
/// Aspect ratio of this sprite (width / height)
pub aspect: f32,
} }
impl crate::Build for Texture { impl crate::Build for Sprite {
type InputSyntax = HashMap<String, syntax::Texture>; type InputSyntaxType = HashMap<String, syntax::Sprite>;
fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(sprites: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (texture_name, t) in texture { for (sprite_name, t) in sprites {
match t { match t {
syntax::Texture::Static(t) => { syntax::Sprite::Static(t) => {
let file = ct.texture_root.join(t.file); let file = ct.image_root.join(&t.file);
let reader = Reader::open(&file).with_context(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read texture `{}` from file `{}`", "Failed to read file `{}` in sprite `{}`",
texture_name, file.display(),
file.display() sprite_name,
) )
})?; })?;
let dim = reader.into_dimensions().with_context(|| { let dim = reader.into_dimensions().with_context(|| {
format!( format!(
"Failed to get dimensions of texture `{}` from file `{}`", "Failed to get dimensions of file `{}` in sprite `{}`",
texture_name, file.display(),
file.display() sprite_name,
) )
})?; })?;
let h = TextureHandle { let h = SpriteHandle {
index: ct.textures.len(), index: ct.sprites.len() as u32,
aspect: dim.0 as f32 / dim.1 as f32, aspect: dim.0 as f32 / dim.1 as f32,
}; };
if texture_name == ct.starfield_texture_name { if sprite_name == ct.starfield_sprite_name {
if ct.starfield_handle.is_none() { if ct.starfield_handle.is_none() {
ct.starfield_handle = Some(h) ct.starfield_handle = Some(h)
} else { } else {
// This can't happen, since this is a hashmap. // This can't happen, since this is a hashmap.
unreachable!("Found two starfield textures! Something is very wrong.") unreachable!("Found two starfield sprites! Something is very wrong.")
} }
} }
ct.texture_index.insert(texture_name.clone(), h); ct.sprite_index.insert(sprite_name.clone(), h);
ct.textures.push(Self { ct.sprites.push(Self {
name: texture_name, name: sprite_name,
frames: vec![file], frames: vec![t.file],
fps: 0.0, fps: 0.0,
handle: h, handle: h,
repeat: RepeatMode::Once, repeat: RepeatMode::Once,
aspect: dim.0 as f32 / dim.1 as f32,
}); });
} }
syntax::Texture::Frames(t) => { syntax::Sprite::Frames(t) => {
let mut dim = None; let mut dim = None;
for f in &t.frames { for f in &t.frames {
let file = ct.texture_root.join(f); let file = ct.image_root.join(f);
let reader = Reader::open(&file).with_context(|| { let reader = Reader::open(&file).with_context(|| {
format!( format!(
"Failed to read texture `{}` from file `{}`", "Failed to read file `{}` in sprite `{}`",
texture_name, file.display(),
file.display() sprite_name,
) )
})?; })?;
let d = reader.into_dimensions().with_context(|| { let d = reader.into_dimensions().with_context(|| {
format!( format!(
"Failed to get dimensions of texture `{}` from file `{}`", "Failed to get dimensions of file `{}` in sprite `{}`",
texture_name, file.display(),
file.display() sprite_name,
) )
})?; })?;
match dim { match dim {
@ -148,37 +152,34 @@ impl crate::Build for Texture {
Some(e) => { Some(e) => {
if d != e { if d != e {
bail!( bail!(
"Failed to load frames of texture `{}`. Frames have different sizes `{}`", "Failed to load frames of sprite `{}` because frames have different sizes.",
texture_name, sprite_name,
file.display()
) )
} }
} }
} }
} }
let dim = dim.unwrap();
let h = TextureHandle { let h = SpriteHandle {
index: ct.textures.len(), index: ct.sprites.len() as u32,
aspect: dim.unwrap().0 as f32 / dim.unwrap().1 as f32, aspect: dim.0 as f32 / dim.1 as f32,
}; };
if texture_name == ct.starfield_texture_name { if sprite_name == ct.starfield_sprite_name {
unreachable!("Starfield texture may not be animated") unreachable!("Starfield texture may not be animated")
} }
let fps = t.duration / t.frames.len() as f32; let fps = t.duration / t.frames.len() as f32;
ct.texture_index.insert(texture_name.clone(), h); ct.sprite_index.insert(sprite_name.clone(), h);
ct.textures.push(Self { ct.sprites.push(Self {
name: texture_name, name: sprite_name,
frames: t frames: t.frames,
.frames
.into_iter()
.map(|f| ct.texture_root.join(f))
.collect(),
fps, fps,
handle: h, handle: h,
repeat: t.repeat, repeat: t.repeat,
aspect: dim.0 as f32 / dim.1 as f32,
}); });
} }
} }
@ -187,7 +188,7 @@ impl crate::Build for Texture {
if ct.starfield_handle.is_none() { if ct.starfield_handle.is_none() {
bail!( bail!(
"Could not find a starfield texture (name: `{}`)", "Could not find a starfield texture (name: `{}`)",
ct.starfield_texture_name ct.starfield_sprite_name
) )
} }

View File

@ -2,7 +2,7 @@ use anyhow::{bail, Context, Result};
use cgmath::{Deg, Point3}; use cgmath::{Deg, Point3};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use crate::{handle::TextureHandle, util::Polar, Content}; use crate::{handle::SpriteHandle, util::Polar, Content};
pub(crate) mod syntax { pub(crate) mod syntax {
use serde::Deserialize; use serde::Deserialize;
@ -17,7 +17,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Object { pub struct Object {
pub sprite_texture: String, pub sprite: String,
pub position: Position, pub position: Position,
pub size: f32, pub size: f32,
@ -94,7 +94,7 @@ pub struct System {
#[derive(Debug)] #[derive(Debug)]
pub struct Object { pub struct Object {
/// This object's sprite /// This object's sprite
pub sprite_texture: TextureHandle, pub sprite: SpriteHandle,
/// This object's size. /// This object's size.
/// Measured as height in game units. /// Measured as height in game units.
@ -175,9 +175,9 @@ fn resolve_position(
} }
impl crate::Build for System { impl crate::Build for System {
type InputSyntax = HashMap<String, syntax::System>; type InputSyntaxType = HashMap<String, syntax::System>;
fn build(system: Self::InputSyntax, ct: &mut Content) -> Result<()> { fn build(system: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (system_name, system) in system { for (system_name, system) in system {
let mut objects = Vec::new(); let mut objects = Vec::new();
@ -185,17 +185,17 @@ impl crate::Build for System {
let mut cycle_detector = HashSet::new(); let mut cycle_detector = HashSet::new();
cycle_detector.insert(label.clone()); cycle_detector.insert(label.clone());
let th = match ct.texture_index.get(&obj.sprite_texture) { let handle = match ct.sprite_index.get(&obj.sprite) {
None => bail!( None => bail!(
"In system `{}`: texture `{}` doesn't exist", "In system `{}`: sprite `{}` doesn't exist",
system_name, system_name,
obj.sprite_texture obj.sprite
), ),
Some(t) => *t, Some(t) => *t,
}; };
objects.push(Object { objects.push(Object {
sprite_texture: th, sprite: handle,
position: resolve_position(&system.object, &obj, cycle_detector) position: resolve_position(&system.object, &obj, cycle_detector)
.with_context(|| format!("In object {:#?}", label))?, .with_context(|| format!("In object {:#?}", label))?,
size: obj.size, size: obj.size,

View File

@ -1,5 +1,5 @@
[[bin]] [[bin]]
name = "galactic" name = "galactica"
path = "src/main.rs" path = "src/main.rs"
[package] [package]

View File

@ -10,7 +10,7 @@ use galactica_behavior::{behavior, ShipBehavior};
use galactica_constants; use galactica_constants;
use galactica_content as content; use galactica_content as content;
use galactica_gameobject as object; use galactica_gameobject as object;
use galactica_render::{FrameState, ObjectSprite, ParticleBuilder, UiSprite}; use galactica_render::{ObjectSprite, ParticleBuilder, RenderState, UiSprite};
use galactica_ui as ui; use galactica_ui as ui;
use galactica_world::{util, ShipPhysicsHandle, World}; use galactica_world::{util, ShipPhysicsHandle, World};
@ -174,14 +174,15 @@ impl Game {
self.last_update = Instant::now(); self.last_update = Instant::now();
} }
pub fn get_frame_state(&mut self) -> FrameState { pub fn get_frame_state(&mut self) -> RenderState {
FrameState { RenderState {
camera_pos: self.camera.pos, camera_pos: self.camera.pos,
camera_zoom: self.camera.zoom, camera_zoom: self.camera.zoom,
object_sprites: self.get_object_sprites(), object_sprites: self.get_object_sprites(),
ui_sprites: self.get_ui_sprites(), ui_sprites: self.get_ui_sprites(),
new_particles: &mut self.new_particles, new_particles: &mut self.new_particles,
current_time: self.start_instant.elapsed().as_secs_f32(), current_time: self.start_instant.elapsed().as_secs_f32(),
content: &self.content,
} }
} }

View File

@ -17,14 +17,15 @@ fn main() -> Result<()> {
// TODO: error if missing // TODO: error if missing
let content = content::Content::load_dir( let content = content::Content::load_dir(
PathBuf::from(galactica_constants::CONTENT_ROOT), PathBuf::from(galactica_constants::CONTENT_ROOT),
PathBuf::from(galactica_constants::TEXTURE_ROOT), PathBuf::from(galactica_constants::IMAGE_ROOT),
PathBuf::from("spriteatlas.toml"), PathBuf::from("spriteatlas.toml"),
galactica_constants::STARFIELD_TEXTURE_NAME.to_owned(), galactica_constants::STARFIELD_SPRITE_NAME.to_owned(),
)?; )?;
let event_loop = EventLoop::new(); let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap(); let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?; let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?;
gpu.init();
let mut game = game::Game::new(content); let mut game = game::Game::new(content);
gpu.update_starfield_buffer(); gpu.update_starfield_buffer();

View File

@ -33,7 +33,7 @@ impl ShipGun {
pub struct OutfitStatSum { pub struct OutfitStatSum {
pub engine_thrust: f32, pub engine_thrust: f32,
pub steer_power: f32, pub steer_power: f32,
pub engine_flare_textures: Vec<content::TextureHandle>, pub engine_flare_sprites: Vec<content::SpriteHandle>,
} }
impl OutfitStatSum { impl OutfitStatSum {
@ -41,23 +41,23 @@ impl OutfitStatSum {
Self { Self {
engine_thrust: 0.0, engine_thrust: 0.0,
steer_power: 0.0, steer_power: 0.0,
engine_flare_textures: Vec::new(), engine_flare_sprites: Vec::new(),
} }
} }
pub fn add(&mut self, o: &content::Outfit) { pub fn add(&mut self, o: &content::Outfit) {
self.engine_thrust += o.engine_thrust; self.engine_thrust += o.engine_thrust;
if let Some(t) = o.engine_flare_texture { if let Some(t) = o.engine_flare_sprite {
self.engine_flare_textures.push(t); self.engine_flare_sprites.push(t);
}; };
self.steer_power += o.steer_power; self.steer_power += o.steer_power;
} }
pub fn remove(&mut self, o: &content::Outfit) { pub fn remove(&mut self, o: &content::Outfit) {
self.engine_thrust -= o.engine_thrust; self.engine_thrust -= o.engine_thrust;
if let Some(t) = o.engine_flare_texture { if let Some(t) = o.engine_flare_sprite {
self.engine_flare_textures.remove( self.engine_flare_sprites.remove(
self.engine_flare_textures self.engine_flare_sprites
.iter() .iter()
.position(|x| *x == t) .position(|x| *x == t)
.unwrap(), .unwrap(),
@ -185,7 +185,7 @@ impl<'a> OutfitSet {
pub fn update_engine_flares(&mut self) { pub fn update_engine_flares(&mut self) {
// TODO: better way to pick flare texture // TODO: better way to pick flare texture
self.engine_flare_sprites.clear(); self.engine_flare_sprites.clear();
let t = if let Some(e) = self.stats.engine_flare_textures.iter().next() { let s = if let Some(e) = self.stats.engine_flare_sprites.iter().next() {
e e
} else { } else {
return; return;
@ -200,7 +200,7 @@ impl<'a> OutfitSet {
y: p.pos.y, y: p.pos.y,
z: 1.0, z: 1.0,
}, },
texture: *t, sprite: *s,
angle: Deg(0.0), angle: Deg(0.0),
size: p.size, size: p.size,
}) })

View File

@ -18,7 +18,7 @@ impl System {
for o in &sys.objects { for o in &sys.objects {
s.bodies.push(SystemObject { s.bodies.push(SystemObject {
pos: o.position, pos: o.position,
sprite_texture: o.sprite_texture, sprite: o.sprite,
size: o.size, size: o.size,
angle: o.angle, angle: o.angle,
}); });

View File

@ -4,7 +4,7 @@ use galactica_content as content;
use galactica_render::ObjectSprite; use galactica_render::ObjectSprite;
pub struct SystemObject { pub struct SystemObject {
pub sprite_texture: content::TextureHandle, pub sprite: content::SpriteHandle,
pub pos: Point3<f32>, pub pos: Point3<f32>,
pub size: f32, pub size: f32,
pub angle: Deg<f32>, pub angle: Deg<f32>,
@ -13,7 +13,7 @@ pub struct SystemObject {
impl SystemObject { impl SystemObject {
pub(crate) fn get_sprite(&self) -> ObjectSprite { pub(crate) fn get_sprite(&self) -> ObjectSprite {
return ObjectSprite { return ObjectSprite {
texture: self.sprite_texture, sprite: self.sprite,
pos: self.pos, pos: self.pos,
angle: self.angle, angle: self.angle,
size: self.size, size: self.size,

View File

@ -1,5 +1,5 @@
use anyhow::{bail, Result}; use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlasImage, SpriteAtlasIndex}; use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use image::{imageops, ImageBuffer, Rgba, RgbaImage}; use image::{imageops, ImageBuffer, Rgba, RgbaImage};
use std::{ use std::{
fs::File, fs::File,
@ -11,6 +11,7 @@ use std::{
// TODO: rework texturearray // TODO: rework texturearray
// TODO: reasonable sprite sizes // TODO: reasonable sprite sizes
// TODO: consistent naming // TODO: consistent naming
// TODO: parallelize
// spriteatlas: the big images // spriteatlas: the big images
// texture: the same, what we load to wgpu // texture: the same, what we load to wgpu
// image: a single file // image: a single file
@ -28,13 +29,16 @@ pub struct AtlasSet {
texture_limit: usize, texture_limit: usize,
/// Keeps track of image files /// Keeps track of image files
index: SpriteAtlasIndex, index: SpriteAtlas,
/// Array of textures, grows as needed /// Array of textures, grows as needed
texture_list: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>, texture_list: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>,
/// The size of the smallest image that didn't fit in each texture /// The size of the smallest image that didn't fit in each texture
image_max_sizes: Vec<u32>, image_max_sizes: Vec<[u32; 2]>,
/// (y-value, image size)
image_y_start: Vec<(u32, [u32; 2])>,
/// A list of used regions in each texture /// A list of used regions in each texture
/// Format: ([xpos, ypos], [width, height]) /// Format: ([xpos, ypos], [width, height])
@ -43,105 +47,137 @@ pub struct AtlasSet {
/// Used to calculate packing efficiency /// Used to calculate packing efficiency
used_area: f64, used_area: f64,
/// The root directory that contains all image files.
/// Files outside this directory will not be packed.
asset_root: PathBuf,
} }
impl AtlasSet { impl AtlasSet {
pub fn new(texture_width: u32, texture_height: u32, texture_limit: usize) -> Self { pub fn new(
texture_width: u32,
texture_height: u32,
texture_limit: usize,
asset_root: &Path,
) -> Self {
Self { Self {
asset_root: asset_root.to_path_buf(),
texture_width, texture_width,
texture_height, texture_height,
texture_limit, texture_limit,
texture_list: Vec::new(), texture_list: Vec::new(),
image_max_sizes: Vec::new(), image_max_sizes: Vec::new(),
used_regions: Vec::new(), used_regions: Vec::new(),
index: SpriteAtlasIndex::new(), index: SpriteAtlas::new(),
used_area: 0f64, used_area: 0f64,
image_y_start: Vec::new(),
} }
} }
/// Returns true if new and fixed overlap,
/// or if new exits the atlas.
/// Parameters: ([xpos, ypos], [width, height])
pub fn boxes_overlap(&self, fixed: ([u32; 2], [u32; 2]), new: ([u32; 2], [u32; 2])) -> bool {
if new.0[0] + new.1[0] >= self.texture_width || new.0[1] + new.1[1] >= self.texture_height {
return true;
}
return fixed.0[0] <= new.0[0] + new.1[0]
&& fixed.0[0] + fixed.1[0] >= new.0[0]
&& fixed.0[1] <= new.0[1] + new.1[1]
&& fixed.0[1] + fixed.1[1] >= new.0[1];
}
/// Add a sprite to this atlas set /// Add a sprite to this atlas set
pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> { pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> {
let mut f = File::open(&path)?; let mut f = File::open(&path)?;
let mut bytes = Vec::new(); let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?; f.read_to_end(&mut bytes)?;
let img = image::load_from_memory(&bytes)?; let img = image::load_from_memory(&bytes)?;
let mut pixel_idx = 0;
let mut atlas_idx = 0;
// Find first available region, starting at top-left of atlas 0. let mut x = 0;
// Includes a few speed optimizations let mut y = 0;
loop { let mut final_atlas_idx = None;
// Loop over atlas textures
'outer: for atlas_idx in 0..self.texture_limit {
if atlas_idx >= self.texture_list.len() { if atlas_idx >= self.texture_list.len() {
// We can't start another atlas, we're at the limit
if atlas_idx >= self.texture_limit {
// TODO: how does a user resolve this?
bail!("Sprites didn't fit into atlas");
}
// Start a new atlas // Start a new atlas
self.texture_list self.texture_list
.push(RgbaImage::new(self.texture_width, self.texture_height)); .push(RgbaImage::new(self.texture_width, self.texture_height));
self.used_regions.push(Vec::new()); self.used_regions.push(Vec::new());
self.image_max_sizes.push(u32::MAX) self.image_max_sizes.push([u32::MAX, u32::MAX]);
self.image_y_start.push((0, [u32::MAX, u32::MAX]));
} }
let x = pixel_idx % self.texture_width; // Optimization: save the smallest sprite that didn't fit in each atlas,
let y = pixel_idx / self.texture_height; // and don't try to add similarly-sized sprites.
let new = ([x, y], dim); if dim[0] >= self.image_max_sizes[atlas_idx][0]
let mut used = false; && dim[1] >= self.image_max_sizes[atlas_idx][1]
for r in &self.used_regions[atlas_idx] { {
if self.boxes_overlap(*r, new) { continue 'outer;
// Speed boost: skip the whole box }
pixel_idx += new.1[0] - 1;
used = true; x = 0;
break; y = 0;
if self.image_y_start.len() != 0 {
let (sy, sd) = self.image_y_start[atlas_idx];
if dim[0] >= sd[0] || dim[1] >= sd[1] {
y = sy;
}
} else {
self.image_y_start.push((0, [u32::MAX, u32::MAX]));
}
let mut free = false;
let mut new;
'inner: while y < self.texture_height && !free {
new = ([x, y], dim);
free = true;
for r in &self.used_regions[atlas_idx] {
// If boxes overlap...
if r.0[0] < new.0[0] + new.1[0]
&& r.0[0] + r.1[0] > new.0[0]
&& r.0[1] < new.0[1] + new.1[1]
&& r.0[1] + r.1[1] > new.0[1]
{
// Skip the whole occupied area
x = r.1[0] + r.0[0];
if x + dim[0] >= self.texture_width {
y += 1;
x = 0;
}
free = false;
continue 'inner;
}
} }
} }
if !used { if y + dim[1] >= self.texture_height {
// This sprite didn't fit, move on to the next atlas
self.image_max_sizes[atlas_idx] = [dim[0] / 2, dim[1] / 2];
} else if free {
final_atlas_idx = Some(atlas_idx);
break; break;
} }
pixel_idx += 1;
// Speed boost: save the smallest sprite that didn't fit in each atlas,
// and don't even try to add bigger sprite.
if dim[0] * dim[1] >= self.image_max_sizes[atlas_idx] {
atlas_idx += 1;
pixel_idx = 0;
}
// This sprite didn't fit, move on to the next atlas
if pixel_idx >= self.texture_width * self.texture_height {
self.image_max_sizes[atlas_idx] = dim[0] * dim[1];
atlas_idx += 1;
pixel_idx = 0;
}
} }
let atlas_idx = match final_atlas_idx {
None => bail!("textures didn't fit!"),
Some(s) => s,
};
// We found a spot for this image, write it. // We found a spot for this image, write it.
let x = pixel_idx % self.texture_width; //let img = RgbaImage::from_pixel(dim[0], dim[1], Rgba([0, 0, 0, 255]));
let y = pixel_idx / self.texture_height;
imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into()); imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into());
self.used_regions[atlas_idx].push(([x, y], dim)); self.used_regions[atlas_idx].push(([x, y], dim));
self.used_area += dim[0] as f64 * dim[1] as f64; self.used_area += dim[0] as f64 * dim[1] as f64;
self.index.insert( let (sy, sd) = self.image_y_start[atlas_idx];
path.to_path_buf(), if dim[0] <= sd[0] && dim[1] <= sd[1] {
// Reset start y if both dimensions of this texture are smaller than the previous smallest texture
// We check for both, because that ensures that the smaller texture can tile the previous largest one.
self.image_y_start[atlas_idx] = (0, [dim[0] / 2, dim[1] / 2]);
} else {
self.image_y_start[atlas_idx] = (y.max(sy), sd);
}
let p = path.strip_prefix(&self.asset_root).with_context(|| {
format!(
"path `{}` is not relative to asset root `{}`",
path.display(),
self.asset_root.display()
)
})?;
self.index.index.insert(
p.to_path_buf(),
SpriteAtlasImage { SpriteAtlasImage {
atlas: atlas_idx, atlas: atlas_idx,
x: x as f32 / self.texture_width as f32, x: x as f32 / self.texture_width as f32,

View File

@ -3,10 +3,7 @@
//! This crate creates texture atlases from an asset tree. //! This crate creates texture atlases from an asset tree.
//! The main interface for this crate is ... TODO //! The main interface for this crate is ... TODO
use std::{ use std::{collections::HashMap, path::PathBuf};
collections::HashMap,
path::{Path, PathBuf},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -36,26 +33,16 @@ pub struct SpriteAtlasImage {
/// A map between file paths (relative to the root asset dir) /// A map between file paths (relative to the root asset dir)
/// and [`AtlasTexture`]s. /// and [`AtlasTexture`]s.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SpriteAtlasIndex { pub struct SpriteAtlas {
pub(crate) index: HashMap<PathBuf, SpriteAtlasImage>, /// The images in this atlas
pub index: HashMap<PathBuf, SpriteAtlasImage>,
} }
impl SpriteAtlasIndex { impl SpriteAtlas {
/// Make an empty [`SpriteAtlasIndex`] /// Make an empty [`SpriteAtlasIndex`]
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
index: HashMap::new(), index: HashMap::new(),
} }
} }
/// Make an empty [`SpriteAtlasIndex`]
pub fn insert(&mut self, path: PathBuf, atlasimage: SpriteAtlasImage) {
self.index.insert(path, atlasimage);
}
/// Get an [`AtlasImage`] for a file `p`.
/// Paths must be relative to the root of the asset directory.
pub fn get(&self, p: &Path) -> Option<&SpriteAtlasImage> {
self.index.get(p)
}
} }

View File

@ -2,37 +2,31 @@ mod atlasset;
use atlasset::AtlasSet; use atlasset::AtlasSet;
use anyhow::Result; use anyhow::{bail, Result};
use image::io::Reader; use image::io::Reader;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use walkdir::WalkDir; use walkdir::WalkDir;
// TODO: procedural sun coloring // TODO: warning when images have extra transparency
// TODO: transparency buffer
// TODO: don't re-encode. Direct to gpu? // TODO: don't re-encode. Direct to gpu?
// (maybe not, tiling is slow. Make it work with files first.) // (maybe not, tiling is slow. Make it work with files first.)
// TODO: path for atlas files // TODO: path for atlas files
// TODO: rework texturearray
// TODO: reasonable sprite sizes (especially ui, document rules)
// TODO: consistent naming
// TODO: dynamic packing (for plugins) // TODO: dynamic packing (for plugins)
// spriteatlas: the set of textures
// texture: a single plane of many images, what we load to wgpu
// image: a single file
// sprite: a possibly animated texture
fn main() -> Result<()> { fn main() -> Result<()> {
let mut files = Vec::new(); let mut files = Vec::new();
for e in WalkDir::new("./assets/render") let asset_root = Path::new("./assets/render");
.into_iter()
.filter_map(|e| e.ok()) // Total number of pixels we want to add
{ let mut total_dim = 0f64;
for e in WalkDir::new(&asset_root).into_iter().filter_map(|e| e.ok()) {
if e.metadata().unwrap().is_file() { if e.metadata().unwrap().is_file() {
// TODO: better warnings // TODO: better warnings
match e.path().extension() { match e.path().extension() {
Some(t) => { Some(t) => {
if t.to_str() != Some("png") { if t.to_str() != Some("png") && t.to_str() != Some("jpg") {
println!("[WARNING] {e:#?} is not a png file, skipping."); println!("[WARNING] {e:#?} is not a png file, skipping.");
continue; continue;
} }
@ -46,7 +40,8 @@ fn main() -> Result<()> {
let path = e.path().to_path_buf(); let path = e.path().to_path_buf();
let reader = Reader::open(&path)?; let reader = Reader::open(&path)?;
let dim = reader.into_dimensions()?; let dim = reader.into_dimensions()?;
files.push((path, [dim.0, dim.1])) files.push((path, [dim.0, dim.1]));
total_dim += dim.0 as f64 * dim.1 as f64;
} }
} }
@ -57,10 +52,19 @@ fn main() -> Result<()> {
b.cmp(&a) b.cmp(&a)
}); });
// Make sure we have enough pixels.
// This check is conservative and imperfect:
// Our tiling algorithm usually has efficiency better than 80% (~90%, as of writing)
// We need room for error, though, since this check doesn't guarante success.
if total_dim / 0.80 >= (8192.0 * 8192.0 * 16.0) {
bail!("Texture atlas is too small")
}
// Create atlas set // Create atlas set
let mut atlas_set = AtlasSet::new(8192, 8192, 16); let mut atlas_set = AtlasSet::new(8192, 8192, 16, &asset_root);
let total = files.len(); let total = files.len();
let mut i = 0; let mut i = 0;
let mut peak_efficiency = 0f64;
for (path, dim) in files { for (path, dim) in files {
i += 1; i += 1;
let atlas_idx = atlas_set.write_image(&path, dim)?; let atlas_idx = atlas_set.write_image(&path, dim)?;
@ -69,6 +73,7 @@ fn main() -> Result<()> {
100.0 * atlas_set.get_efficiency(), 100.0 * atlas_set.get_efficiency(),
path.display() path.display()
); );
peak_efficiency = peak_efficiency.max(atlas_set.get_efficiency());
} }
println!( println!(
@ -76,9 +81,11 @@ fn main() -> Result<()> {
100.0 * atlas_set.get_efficiency() 100.0 * atlas_set.get_efficiency()
); );
println!("Peak efficiency: {:.02}%", 100.0 * peak_efficiency);
println!("Saving files..."); println!("Saving files...");
atlas_set.save_files( atlas_set.save_files(
|x| PathBuf::from(format!("atlas-{x:0.2}.png")), |x| PathBuf::from(format!("atlas-{x:0.2}.bmp")),
&PathBuf::from("spriteatlas.toml"), &PathBuf::from("spriteatlas.toml"),
)?; )?;

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>, @location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>, @location(3) transform_matrix_1: vec4<f32>,
@ -9,27 +11,12 @@ struct InstanceInput {
struct VertexInput { struct VertexInput {
@location(0) position: vec3<f32>, @location(0) position: vec3<f32>,
@location(1) texture_coords: vec2<f32>, @location(1) texture_coords: vec2<f32>,
} };
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>, @location(0) texture_coords: vec2<f32>,
@location(1) texture_index: u32, @location(1) texture_index: u32,
}
@group(1) @binding(0)
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>,
current_time: vec2<f32>,
}; };
@ -56,8 +43,18 @@ fn vertex_main(
var out: VertexOutput; var out: VertexOutput;
out.position = transform * vec4<f32>(vertex.position, 1.0); out.position = transform * vec4<f32>(vertex.position, 1.0);
out.texture_coords = vertex.texture_coords;
out.texture_index = instance.texture_index; let i = sprites.data[instance.texture_index].first_frame;
let t = atlas.data[i];
out.texture_index = u32(0);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {
out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y);
}
if vertex.texture_coords.y == 1.0 {
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height);
}
return out; return out;
} }

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) position: vec2<f32>, @location(2) position: vec2<f32>,
@location(3) velocity: vec2<f32>, @location(3) velocity: vec2<f32>,
@ -6,8 +8,7 @@ struct InstanceInput {
@location(6) size: f32, @location(6) size: f32,
@location(7) created: f32, @location(7) created: f32,
@location(8) expires: f32, @location(8) expires: f32,
@location(9) texture_index_len_rep: vec3<u32>, @location(9) texture_index: u32,
@location(10) texture_aspect_fps: vec2<f32>,
}; };
struct VertexInput { struct VertexInput {
@ -22,21 +23,6 @@ struct VertexOutput {
} }
@group(1) @binding(0)
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>,
current_time: vec2<f32>,
};
@group(0) @binding(0) @group(0) @binding(0)
var texture_array: binding_array<texture_2d<f32>>; var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
@ -57,37 +43,51 @@ fn vertex_main(
out.texture_coords = vertex.texture_coords; out.texture_coords = vertex.texture_coords;
if instance.expires < global.current_time.x { if instance.expires < global.current_time.x {
out.texture_index = instance.texture_index_len_rep.x; out.texture_index = u32(0);
out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0); out.position = vec4<f32>(2.0, 2.0, 0.0, 1.0);
return out; return out;
} }
let age = global.current_time.x - instance.created; let age = global.current_time.x - instance.created;
let len = sprites.data[instance.texture_index].frame_count;
let rep = sprites.data[instance.texture_index].repeatmode;
let fps = sprites.data[instance.texture_index].fps;
var frame: u32 = u32(0); var frame: u32 = u32(0);
if instance.texture_index_len_rep.z == u32(1) { if rep == u32(1) {
// Repeat // Repeat
frame = u32(fmod( frame = u32(fmod(
(age / instance.texture_aspect_fps.y), (age / fps),
f32(instance.texture_index_len_rep.y) f32(len)
)); ));
} else { } else {
// Once // Once
frame = u32(min( frame = u32(min(
(age / instance.texture_aspect_fps.y), (age / fps),
f32(instance.texture_index_len_rep.y) - 1.0 f32(len) - 1.0
)); ));
} }
out.texture_index = instance.texture_index_len_rep.x + frame; // Pick image
frame = frame + sprites.data[instance.texture_index].first_frame;
let t = atlas.data[frame];
out.texture_index = u32(0);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {
out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y);
}
if vertex.texture_coords.y == 1.0 {
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height);
}
let rotation = mat2x2(instance.rotation_0, instance.rotation_1); let rotation = mat2x2(instance.rotation_0, instance.rotation_1);
var scale: f32 = instance.size / global.camera_zoom.x; var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y); var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
pos = pos * vec2<f32>( pos = pos * vec2<f32>(
instance.texture_aspect_fps.x * scale / global.window_aspect.x, sprites.data[instance.texture_index].aspect * scale / global.window_aspect.x,
scale scale
); );
pos = rotation * pos; pos = rotation * pos;

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) position: vec3<f32>, @location(2) position: vec3<f32>,
@location(3) size: f32, @location(3) size: f32,
@ -12,24 +14,10 @@ struct VertexInput {
struct VertexOutput { struct VertexOutput {
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>, @location(0) texture_coords: vec2<f32>,
@location(1) tint: vec2<f32>, @location(1) texture_index: u32,
@location(2) tint: vec2<f32>,
} }
@group(1) @binding(0)
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>,
current_time: vec2<f32>,
};
@group(0) @binding(0) @group(0) @binding(0)
var texture_array: binding_array<texture_2d<f32>>; var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
@ -49,7 +37,6 @@ fn vertex_main(
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.texture_coords = vertex.texture_coords;
out.tint = instance.tint; out.tint = instance.tint;
// Center of the tile the camera is currently in, in game coordinates. // Center of the tile the camera is currently in, in game coordinates.
@ -120,6 +107,18 @@ fn vertex_main(
); );
out.position = vec4<f32>(pos, 0.0, 1.0) * instance.position.z; out.position = vec4<f32>(pos, 0.0, 1.0) * instance.position.z;
let i = sprites.data[global.starfield_sprite.x].first_frame;
let t = atlas.data[i];
out.texture_index = u32(0);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {
out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y);
}
if vertex.texture_coords.y == 1.0 {
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height);
}
return out; return out;
} }
@ -136,7 +135,7 @@ fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
let c_del = c_bot - c_top; let c_del = c_bot - c_top;
return textureSampleLevel( return textureSampleLevel(
texture_array[global.starfield_texture.x], texture_array[in.texture_index],
sampler_array[0], sampler_array[0],
in.texture_coords, in.texture_coords,
0.0 0.0

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput { struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>, @location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>, @location(3) transform_matrix_1: vec4<f32>,
@ -19,22 +21,6 @@ struct VertexOutput {
@location(2) color_transform: vec4<f32>, @location(2) color_transform: vec4<f32>,
} }
@group(1) @binding(0)
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>,
current_time: vec2<f32>,
};
@group(0) @binding(0) @group(0) @binding(0)
var texture_array: binding_array<texture_2d<f32>>; var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1) @group(0) @binding(1)
@ -58,9 +44,19 @@ fn vertex_main(
var out: VertexOutput; var out: VertexOutput;
out.position = transform * vec4<f32>(vertex.position, 1.0); out.position = transform * vec4<f32>(vertex.position, 1.0);
out.texture_coords = vertex.texture_coords;
out.texture_index = instance.texture_index;
out.color_transform = instance.color_transform; out.color_transform = instance.color_transform;
let i = sprites.data[instance.texture_index].first_frame;
let t = atlas.data[i];
out.texture_index = u32(0);
out.texture_coords = vec2(t.xpos, t.ypos);
if vertex.texture_coords.x == 1.0 {
out.texture_coords = vec2(out.texture_coords.x + t.width, out.texture_coords.y);
}
if vertex.texture_coords.y == 1.0 {
out.texture_coords = vec2(out.texture_coords.x, out.texture_coords.y + t.height);
}
return out; return out;
} }

View File

@ -1,96 +0,0 @@
use bytemuck;
use std::mem;
use wgpu;
pub struct GlobalData {
pub buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: wgpu::BindGroupLayout,
pub content: GlobalDataContent,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
// Uniforms require uniform alignment.
// Since the largest value in this array is a [f32; 2],
// all smaller values must be padded.
// also, [f32; 3] are aligned as [f32; 4]
// (since alignments must be powers of two)
pub struct GlobalDataContent {
/// Camera position, in game units
pub camera_position: [f32; 2],
/// Camera zoom value, in game units.
/// 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 ratio of window
/// Second component is ignored.
pub window_aspect: [f32; 2],
/// Texture index of starfield sprites
/// Second component is ignored.
pub starfield_texture: [u32; 2],
// Size of (square) starfield tiles, in game units
/// 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],
/// Current game time, in seconds.
/// Second component is ignored.
pub current_time: [f32; 2],
}
impl GlobalDataContent {
const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}
impl GlobalData {
pub fn new(device: &wgpu::Device) -> Self {
let buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: None,
size: GlobalDataContent::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("globaldata bind group layout"),
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
label: Some("globaldata bind group"),
});
return Self {
buffer,
bind_group,
bind_group_layout,
content: GlobalDataContent::default(),
};
}
}

View File

@ -0,0 +1,43 @@
use bytemuck::{Pod, Zeroable};
use std::mem;
use wgpu;
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable, Default)]
pub struct ImageLocation {
// Image box, in texture coordinates
pub xpos: f32,
pub ypos: f32,
pub width: f32,
pub height: f32,
}
#[derive(Debug, Copy, Clone)]
pub struct ImageLocationArray {
pub data: [ImageLocation; 108],
}
unsafe impl Pod for ImageLocationArray {}
unsafe impl Zeroable for ImageLocationArray {
fn zeroed() -> Self {
Self {
data: [ImageLocation::zeroed(); 108],
}
}
}
impl Default for ImageLocationArray {
fn default() -> Self {
Self::zeroed()
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, Pod, Zeroable)]
pub struct AtlasContent {
pub data: ImageLocationArray,
}
impl AtlasContent {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -0,0 +1,48 @@
use bytemuck;
use std::mem;
use wgpu;
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, bytemuck::Pod, bytemuck::Zeroable)]
// Uniforms require uniform alignment.
// Since the largest value in this array is a [f32; 2],
// all smaller values must be padded.
// also, [f32; 3] are aligned as [f32; 4]
// (since alignments must be powers of two)
pub struct DataContent {
/// Camera position, in game units
pub camera_position: [f32; 2],
/// Camera zoom value, in game units.
/// 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 ratio of window
/// Second component is ignored.
pub window_aspect: [f32; 2],
/// Index of starfield sprite
/// Second component is ignored.
pub starfield_sprite: [u32; 2],
// Size of (square) starfield tiles, in game units
/// 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],
/// Current game time, in seconds.
/// Second component is ignored.
pub current_time: [f32; 2],
}
impl DataContent {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -0,0 +1,171 @@
use wgpu;
use super::{AtlasContent, DataContent, SpriteContent};
pub struct GlobalUniform {
pub data_buffer: wgpu::Buffer,
pub atlas_buffer: wgpu::Buffer,
pub sprite_buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: wgpu::BindGroupLayout,
pub content: DataContent,
}
impl GlobalUniform {
pub fn shader_header(&self, group: u32) -> String {
let mut out = String::new();
out.push_str(&format!("@group({group}) @binding(0)\n"));
out.push_str(
r#"
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_sprite: vec2<u32>,
starfield_tile_size: vec2<f32>,
starfield_size_limits: vec2<f32>,
current_time: vec2<f32>,
};
"#,
);
out.push_str("\n");
out.push_str(&format!("@group({group}) @binding(1)\n"));
out.push_str(
r#"
var<uniform> atlas: AtlasUniform;
struct ImageLocation {
xpos: f32,
ypos: f32,
width: f32,
height: f32,
};
struct AtlasUniform {
data: array<ImageLocation, 108>,
};
"#,
);
out.push_str("\n");
// TODO: document
// wgpu uniforms require constant item sizes.
// if you get an error like the following,check!
// `Buffer is bound with size 3456 where the shader expects 5184 in group[1] compact index 2`
// More notes are in datacontent
out.push_str(&format!("@group({group}) @binding(2)\n"));
out.push_str(
r#"
var<uniform> sprites: SpriteUniform;
struct SpriteData {
frame_count: u32,
repeatmode: u32,
aspect: f32,
fps: f32,
first_frame: u32,
padding_a: f32,
padding_b: f32,
padding_c: f32,
};
struct SpriteUniform {
data: array<SpriteData, 108>,
};
"#,
);
out.push_str("\n");
return out;
}
pub fn new(device: &wgpu::Device) -> Self {
let data_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform data buffer"),
size: DataContent::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let atlas_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform atlas buffer"),
size: AtlasContent::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let sprite_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("global uniform sprite buffer"),
size: SpriteContent::SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
label: Some("global uniform bind group layout"),
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: data_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: atlas_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: sprite_buffer.as_entire_binding(),
},
],
label: Some("global uniform bind group"),
});
return Self {
data_buffer,
atlas_buffer,
sprite_buffer,
bind_group,
bind_group_layout,
content: DataContent::default(),
};
}
}

View File

@ -0,0 +1,9 @@
mod atlascontent;
mod datacontent;
mod globaluniform;
mod spritecontent;
pub use atlascontent::{AtlasContent, ImageLocation, ImageLocationArray};
pub use datacontent::DataContent;
pub use globaluniform::GlobalUniform;
pub use spritecontent::{SpriteContent, SpriteData, SpriteDataArray};

View File

@ -0,0 +1,48 @@
use bytemuck::{Pod, Zeroable};
use std::mem;
use wgpu;
#[repr(C)]
#[derive(Debug, Copy, Clone, Pod, Zeroable, Default)]
pub struct SpriteData {
// Animation parameters
pub frame_count: u32,
pub repeatmode: u32,
pub aspect: f32,
pub fps: f32,
// Index of first frame in ImageLocationArray
pub first_frame: u32,
// stride must be a multiple of 16
pub _padding: [f32; 3],
}
#[derive(Debug, Copy, Clone)]
pub struct SpriteDataArray {
pub data: [SpriteData; 108],
}
unsafe impl Pod for SpriteDataArray {}
unsafe impl Zeroable for SpriteDataArray {
fn zeroed() -> Self {
Self {
data: [SpriteData::zeroed(); 108],
}
}
}
impl Default for SpriteDataArray {
fn default() -> Self {
Self::zeroed()
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone, Default, Pod, Zeroable)]
pub struct SpriteContent {
pub data: SpriteDataArray,
}
impl SpriteContent {
pub const SIZE: u64 = mem::size_of::<Self>() as wgpu::BufferAddress;
}

View File

@ -8,7 +8,7 @@ use winit::{self, dpi::LogicalSize, window::Window};
use crate::{ use crate::{
content, content,
globaldata::{GlobalData, GlobalDataContent}, globaluniform::{AtlasContent, DataContent, GlobalUniform, SpriteContent},
pipeline::PipelineBuilder, pipeline::PipelineBuilder,
sprite::ObjectSubSprite, sprite::ObjectSubSprite,
starfield::Starfield, starfield::Starfield,
@ -18,7 +18,7 @@ use crate::{
types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance}, types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance},
BufferObject, VertexBuffer, BufferObject, VertexBuffer,
}, },
FrameState, ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX, ObjectSprite, RenderState, UiSprite, OPENGL_TO_WGPU_MATRIX,
}; };
/// A high-level GPU wrapper. Consumes game state, /// A high-level GPU wrapper. Consumes game state,
@ -44,7 +44,7 @@ pub struct GPUState {
starfield: Starfield, starfield: Starfield,
texture_array: TextureArray, texture_array: TextureArray,
global_data: GlobalData, global_uniform: GlobalUniform,
vertex_buffers: VertexBuffers, vertex_buffers: VertexBuffers,
} }
@ -60,6 +60,19 @@ struct VertexBuffers {
particle: Rc<VertexBuffer>, particle: Rc<VertexBuffer>,
} }
/// Preprocess shader files
fn preprocess_shader(
shader: &str,
global_uniform: &GlobalUniform,
global_uniform_group: u32,
) -> String {
// Insert common headers
shader.replace(
"// INCLUDE: global uniform header",
&global_uniform.shader_header(global_uniform_group),
)
}
impl GPUState { impl GPUState {
/// Make a new GPUState that draws on `window` /// Make a new GPUState that draws on `window`
pub async fn new(window: Window, ct: &content::Content) -> Result<Self> { pub async fn new(window: Window, ct: &content::Content) -> Result<Self> {
@ -161,22 +174,26 @@ impl GPUState {
}; };
// Load uniforms // Load uniforms
let global_data = GlobalData::new(&device); let global_uniform = GlobalUniform::new(&device);
let texture_array = TextureArray::new(&device, &queue, ct)?; let texture_array = TextureArray::new(&device, &queue, ct)?;
// Make sure these match the indices in each shader // Make sure these match the indices in each shader
let bind_group_layouts = &[ let bind_group_layouts = &[
&texture_array.bind_group_layout, &texture_array.bind_group_layout,
&global_data.bind_group_layout, &global_uniform.bind_group_layout,
]; ];
// Create render pipelines // Create render pipelines
let object_pipeline = PipelineBuilder::new("object", &device) let object_pipeline = PipelineBuilder::new("object", &device)
.set_shader(include_str!(concat!( .set_shader(&preprocess_shader(
env!("CARGO_MANIFEST_DIR"), &include_str!(concat!(
"/shaders/", env!("CARGO_MANIFEST_DIR"),
"object.wgsl" "/shaders/",
))) "object.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format) .set_format(config.format)
.set_triangle(true) .set_triangle(true)
.set_vertex_buffer(&vertex_buffers.object) .set_vertex_buffer(&vertex_buffers.object)
@ -184,11 +201,15 @@ impl GPUState {
.build(); .build();
let starfield_pipeline = PipelineBuilder::new("starfield", &device) let starfield_pipeline = PipelineBuilder::new("starfield", &device)
.set_shader(include_str!(concat!( .set_shader(&preprocess_shader(
env!("CARGO_MANIFEST_DIR"), &include_str!(concat!(
"/shaders/", env!("CARGO_MANIFEST_DIR"),
"starfield.wgsl" "/shaders/",
))) "starfield.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format) .set_format(config.format)
.set_triangle(true) .set_triangle(true)
.set_vertex_buffer(&vertex_buffers.starfield) .set_vertex_buffer(&vertex_buffers.starfield)
@ -196,11 +217,11 @@ impl GPUState {
.build(); .build();
let ui_pipeline = PipelineBuilder::new("ui", &device) let ui_pipeline = PipelineBuilder::new("ui", &device)
.set_shader(include_str!(concat!( .set_shader(&preprocess_shader(
env!("CARGO_MANIFEST_DIR"), &include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/", "ui.wgsl")),
"/shaders/", &global_uniform,
"ui.wgsl" 1,
))) ))
.set_format(config.format) .set_format(config.format)
.set_triangle(true) .set_triangle(true)
.set_vertex_buffer(&vertex_buffers.ui) .set_vertex_buffer(&vertex_buffers.ui)
@ -208,11 +229,15 @@ impl GPUState {
.build(); .build();
let particle_pipeline = PipelineBuilder::new("particle", &device) let particle_pipeline = PipelineBuilder::new("particle", &device)
.set_shader(include_str!(concat!( .set_shader(&preprocess_shader(
env!("CARGO_MANIFEST_DIR"), &include_str!(concat!(
"/shaders/", env!("CARGO_MANIFEST_DIR"),
"particle.wgsl" "/shaders/",
))) "particle.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format) .set_format(config.format)
.set_triangle(true) .set_triangle(true)
.set_vertex_buffer(&vertex_buffers.particle) .set_vertex_buffer(&vertex_buffers.particle)
@ -239,7 +264,7 @@ impl GPUState {
starfield, starfield,
texture_array, texture_array,
global_data, global_uniform,
vertex_buffers, vertex_buffers,
}); });
} }
@ -267,8 +292,7 @@ impl GPUState {
/// Also handles child sprites. /// Also handles child sprites.
fn push_object_sprite( fn push_object_sprite(
&self, &self,
camera_zoom: f32, state: &RenderState,
camera_pos: Point2<f32>,
instances: &mut Vec<ObjectInstance>, instances: &mut Vec<ObjectInstance>,
clip_ne: Point2<f32>, clip_ne: Point2<f32>,
clip_sw: Point2<f32>, clip_sw: Point2<f32>,
@ -280,10 +304,9 @@ impl GPUState {
(Point2 { (Point2 {
x: s.pos.x, x: s.pos.x,
y: s.pos.y, y: s.pos.y,
} - camera_pos.to_vec()) } - state.camera_pos.to_vec())
/ s.pos.z / s.pos.z
}; };
let texture = self.texture_array.get_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.
@ -291,7 +314,7 @@ impl GPUState {
// Width or height, whichever is larger. // Width or height, whichever is larger.
// Accounts for sprite rotation. // Accounts for sprite rotation.
let m = height * texture.aspect.max(1.0); let m = height * s.sprite.aspect.max(1.0);
// Don't draw (or compute matrices for) // Don't draw (or compute matrices for)
// sprites that are off the screen // sprites that are off the screen
@ -304,7 +327,7 @@ impl GPUState {
} }
// TODO: clean up // TODO: clean up
let scale = height / camera_zoom; let scale = height / state.camera_zoom;
// Note that our mesh starts centered at (0, 0). // Note that our mesh starts centered at (0, 0).
// This is essential---we do not want scale and rotation // This is essential---we do not want scale and rotation
@ -315,7 +338,7 @@ impl GPUState {
// //
// We apply the provided scale here as well as a minor optimization // We apply the provided scale here as well as a minor optimization
let sprite_aspect_and_scale = let sprite_aspect_and_scale =
Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0); Matrix4::from_nonuniform_scale(s.sprite.aspect * scale, scale, 1.0);
// Apply rotation // Apply rotation
let rotate = Matrix4::from_angle_z(s.angle); let rotate = Matrix4::from_angle_z(s.angle);
@ -333,8 +356,8 @@ impl GPUState {
// The height of the viewport is `zoom` in game units, // The height of the viewport is `zoom` in game units,
// but it's 2 in screen units! (since coordinates range from -1 to 1) // but it's 2 in screen units! (since coordinates range from -1 to 1)
let translate = Matrix4::from_translation(Vector3 { let translate = Matrix4::from_translation(Vector3 {
x: pos.x / (camera_zoom / 2.0) / self.window_aspect, x: pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: pos.y / (camera_zoom / 2.0), y: pos.y / (state.camera_zoom / 2.0),
z: 0.0, z: 0.0,
}); });
@ -345,13 +368,13 @@ impl GPUState {
instances.push(ObjectInstance { instances.push(ObjectInstance {
transform: t.into(), transform: t.into(),
texture_index: texture.index, sprite_index: s.sprite.index,
}); });
// Add children // Add children
if let Some(children) = &s.children { if let Some(children) = &s.children {
for c in children { for c in children {
self.push_object_subsprite(camera_zoom, instances, c, pos, s.angle); self.push_object_subsprite(&state, instances, c, pos, s.angle);
} }
} }
} }
@ -360,29 +383,28 @@ impl GPUState {
/// Only called by `self.push_object_sprite`. /// Only called by `self.push_object_sprite`.
fn push_object_subsprite( fn push_object_subsprite(
&self, &self,
camera_zoom: f32, state: &RenderState,
instances: &mut Vec<ObjectInstance>, instances: &mut Vec<ObjectInstance>,
s: &ObjectSubSprite, s: &ObjectSubSprite,
parent_pos: Point2<f32>, parent_pos: Point2<f32>,
parent_angle: Deg<f32>, parent_angle: Deg<f32>,
) { ) {
let texture = self.texture_array.get_texture(s.texture); let scale = s.size / (s.pos.z * state.camera_zoom);
let scale = s.size / (s.pos.z * camera_zoom);
let sprite_aspect_and_scale = let sprite_aspect_and_scale =
Matrix4::from_nonuniform_scale(texture.aspect * scale, scale, 1.0); Matrix4::from_nonuniform_scale(s.sprite.aspect * scale, scale, 1.0);
let rotate = Matrix4::from_angle_z(s.angle); let rotate = Matrix4::from_angle_z(s.angle);
let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0); let screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0);
let ptranslate = Matrix4::from_translation(Vector3 { let ptranslate = Matrix4::from_translation(Vector3 {
x: parent_pos.x / (camera_zoom / 2.0) / self.window_aspect, x: parent_pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: parent_pos.y / (camera_zoom / 2.0), y: parent_pos.y / (state.camera_zoom / 2.0),
z: 0.0, z: 0.0,
}); });
let protate = Matrix4::from_angle_z(parent_angle); let protate = Matrix4::from_angle_z(parent_angle);
let translate = Matrix4::from_translation(Vector3 { let translate = Matrix4::from_translation(Vector3 {
x: s.pos.x / (camera_zoom / 2.0) / self.window_aspect, x: s.pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: s.pos.y / (camera_zoom / 2.0), y: s.pos.y / (state.camera_zoom / 2.0),
z: 0.0, z: 0.0,
}); });
@ -395,7 +417,7 @@ impl GPUState {
instances.push(ObjectInstance { instances.push(ObjectInstance {
transform: t.into(), transform: t.into(),
texture_index: texture.index, sprite_index: s.sprite.index,
}); });
} }
@ -404,7 +426,6 @@ impl GPUState {
let logical_size: LogicalSize<f32> = let logical_size: LogicalSize<f32> =
self.window_size.to_logical(self.window.scale_factor()); self.window_size.to_logical(self.window.scale_factor());
let texture = self.texture_array.get_texture(s.texture);
let width = s.dimensions.x; let width = s.dimensions.x;
let height = s.dimensions.y; let height = s.dimensions.y;
@ -448,7 +469,7 @@ impl GPUState {
instances.push(UiInstance { instances.push(UiInstance {
transform: (OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale).into(), transform: (OPENGL_TO_WGPU_MATRIX * translate * screen_aspect * rotate * scale).into(),
texture_index: texture.index, sprite_index: s.sprite.index,
color: s.color.unwrap_or([1.0, 1.0, 1.0, 1.0]), color: s.color.unwrap_or([1.0, 1.0, 1.0, 1.0]),
}); });
} }
@ -456,23 +477,16 @@ impl GPUState {
/// Make an instance for all the game's sprites /// Make an instance for all the game's sprites
/// (Objects and UI) /// (Objects and UI)
/// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded. /// This will Will panic if any X_SPRITE_INSTANCE_LIMIT is exceeded.
fn update_sprite_instances(&self, framestate: FrameState) -> (usize, usize) { fn update_sprite_instances(&self, state: &RenderState) -> (usize, usize) {
let mut object_instances: Vec<ObjectInstance> = Vec::new(); let mut object_instances: Vec<ObjectInstance> = Vec::new();
// Game coordinates (relative to camera) of ne and sw corners of screen. // Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites. // Used to skip off-screen sprites.
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * framestate.camera_zoom; let clip_ne = Point2::from((-self.window_aspect, 1.0)) * state.camera_zoom;
let clip_sw = Point2::from((self.window_aspect, -1.0)) * framestate.camera_zoom; let clip_sw = Point2::from((self.window_aspect, -1.0)) * state.camera_zoom;
for s in framestate.object_sprites { for s in &state.object_sprites {
self.push_object_sprite( self.push_object_sprite(state, &mut object_instances, clip_ne, clip_sw, &s);
framestate.camera_zoom,
framestate.camera_pos,
&mut object_instances,
clip_ne,
clip_sw,
&s,
);
} }
// Enforce sprite limit // Enforce sprite limit
@ -489,7 +503,7 @@ impl GPUState {
let mut ui_instances: Vec<UiInstance> = Vec::new(); let mut ui_instances: Vec<UiInstance> = Vec::new();
for s in framestate.ui_sprites { for s in &state.ui_sprites {
self.push_ui_sprite(&mut ui_instances, &s); self.push_ui_sprite(&mut ui_instances, &s);
} }
@ -518,8 +532,29 @@ impl GPUState {
); );
} }
/// Initialize the rendering engine
pub fn init(&mut self) {
// Update global values
self.queue.write_buffer(
&self.global_uniform.atlas_buffer,
0,
bytemuck::cast_slice(&[AtlasContent {
data: self.texture_array.image_locations,
}]),
);
self.queue.write_buffer(
&self.global_uniform.sprite_buffer,
0,
bytemuck::cast_slice(&[SpriteContent {
data: self.texture_array.sprite_data,
}]),
);
self.update_starfield_buffer();
}
/// Main render function. Draws sprites on a window. /// Main render function. Draws sprites on a window.
pub fn render(&mut self, framestate: FrameState) -> Result<(), wgpu::SurfaceError> { pub fn render(&mut self, state: RenderState) -> Result<(), wgpu::SurfaceError> {
let output = self.surface.get_current_texture()?; let output = self.surface.get_current_texture()?;
let view = output let view = output
.texture .texture
@ -552,32 +587,32 @@ impl GPUState {
timestamp_writes: None, timestamp_writes: None,
}); });
let s = state.content.get_starfield_handle();
// Update global values // Update global values
self.queue.write_buffer( self.queue.write_buffer(
&self.global_data.buffer, &self.global_uniform.data_buffer,
0, 0,
bytemuck::cast_slice(&[GlobalDataContent { bytemuck::cast_slice(&[DataContent {
camera_position: framestate.camera_pos.into(), camera_position: state.camera_pos.into(),
camera_zoom: [framestate.camera_zoom, 0.0], camera_zoom: [state.camera_zoom, 0.0],
camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX], camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::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,
], ],
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_sprite: [s.index, 0],
starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0], starfield_tile_size: [galactica_constants::STARFIELD_SIZE as f32, 0.0],
starfield_size_limits: [ starfield_size_limits: [
galactica_constants::STARFIELD_SIZE_MIN, galactica_constants::STARFIELD_SIZE_MIN,
galactica_constants::STARFIELD_SIZE_MAX, galactica_constants::STARFIELD_SIZE_MAX,
], ],
current_time: [framestate.current_time, 0.0], current_time: [state.current_time, 0.0],
}]), }]),
); );
// Write all new particles to GPU buffer // Write all new particles to GPU buffer
for i in framestate.new_particles.iter() { for i in state.new_particles.iter() {
let texture = self.texture_array.get_texture(i.texture);
self.queue.write_buffer( self.queue.write_buffer(
&self.vertex_buffers.particle.instances, &self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_array_head, ParticleInstance::SIZE * self.vertex_buffers.particle_array_head,
@ -586,10 +621,9 @@ impl GPUState {
velocity: i.velocity.into(), velocity: i.velocity.into(),
rotation: Matrix2::from_angle(i.angle).into(), rotation: Matrix2::from_angle(i.angle).into(),
size: i.size, size: i.size,
texture_index_len_rep: [texture.index, texture.len, texture.repeat], sprite_index: i.sprite.index,
texture_aspect_fps: [texture.aspect, texture.fps], created: state.current_time,
created: framestate.current_time, expires: state.current_time + i.lifetime,
expires: framestate.current_time + i.lifetime,
}]), }]),
); );
self.vertex_buffers.particle_array_head += 1; self.vertex_buffers.particle_array_head += 1;
@ -599,15 +633,15 @@ impl GPUState {
self.vertex_buffers.particle_array_head = 0; self.vertex_buffers.particle_array_head = 0;
} }
} }
framestate.new_particles.clear(); state.new_particles.clear();
// Create sprite instances // Create sprite instances
let (n_object, n_ui) = self.update_sprite_instances(framestate); let (n_object, n_ui) = self.update_sprite_instances(&state);
// These should match the indices in each shader, // These should match the indices in each shader,
// and should each have a corresponding bind group layout. // and should each have a corresponding bind group layout.
render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]); render_pass.set_bind_group(0, &self.texture_array.bind_group, &[]);
render_pass.set_bind_group(1, &self.global_data.bind_group, &[]); render_pass.set_bind_group(1, &self.global_uniform.bind_group, &[]);
// Starfield pipeline // Starfield pipeline
self.vertex_buffers.starfield.set_in_pass(&mut render_pass); self.vertex_buffers.starfield.set_in_pass(&mut render_pass);

View File

@ -7,18 +7,18 @@
//! and the only one external code should interact with. //! and the only one external code should interact with.
//! (Excluding data structs, like [`ObjectSprite`]) //! (Excluding data structs, like [`ObjectSprite`])
mod framestate; mod globaluniform;
mod globaldata;
mod gpustate; mod gpustate;
mod pipeline; mod pipeline;
mod renderstate;
mod sprite; mod sprite;
mod starfield; mod starfield;
mod texturearray; mod texturearray;
mod vertexbuffer; mod vertexbuffer;
pub use framestate::FrameState;
use galactica_content as content; use galactica_content as content;
pub use gpustate::GPUState; pub use gpustate::GPUState;
pub use renderstate::RenderState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite}; pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite};
use cgmath::Matrix4; use cgmath::Matrix4;

View File

@ -1,9 +1,10 @@
use cgmath::Point2; use cgmath::Point2;
use galactica_content::Content;
use crate::{ObjectSprite, ParticleBuilder, UiSprite}; use crate::{ObjectSprite, ParticleBuilder, UiSprite};
/// Bundles parameters passed to a single call to GPUState::render /// Bundles parameters passed to a single call to GPUState::render
pub struct FrameState<'a> { pub struct RenderState<'a> {
/// Camera position, in world units /// Camera position, in world units
pub camera_pos: Point2<f32>, pub camera_pos: Point2<f32>,
@ -23,4 +24,7 @@ pub struct FrameState<'a> {
// TODO: handle overflow // TODO: handle overflow
/// The current time, in seconds /// The current time, in seconds
pub current_time: f32, pub current_time: f32,
/// Game content
pub content: &'a Content,
} }

View File

@ -3,8 +3,8 @@ use cgmath::{Deg, Point2, Point3, Vector2};
/// Instructions to create a new particle /// Instructions to create a new particle
pub struct ParticleBuilder { pub struct ParticleBuilder {
/// The texture to use for this particle /// The sprite to use for this particle
pub texture: content::TextureHandle, pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates. /// This object's center, in world coordinates.
pub pos: Point2<f32>, pub pos: Point2<f32>,
@ -18,7 +18,7 @@ pub struct ParticleBuilder {
/// This particle's lifetime, in seconds /// This particle's lifetime, in seconds
pub lifetime: f32, pub lifetime: f32,
/// The size of this sprite, /// The size of this particle,
/// given as height in world units. /// given as height in world units.
pub size: f32, pub size: f32,
} }
@ -54,8 +54,8 @@ pub enum AnchoredUiPosition {
/// A sprite that represents a ui element /// A sprite that represents a ui element
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct UiSprite { pub struct UiSprite {
/// The texture to use for this sprite /// The sprite to draw
pub texture: content::TextureHandle, pub sprite: content::SpriteHandle,
/// This object's position, in logical (dpi-adjusted) pixels /// This object's position, in logical (dpi-adjusted) pixels
pub pos: AnchoredUiPosition, pub pos: AnchoredUiPosition,
@ -75,8 +75,8 @@ pub struct UiSprite {
/// Ships, planets, debris, etc /// Ships, planets, debris, etc
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjectSprite { pub struct ObjectSprite {
/// The texture to use for this sprite /// The sprite to draw
pub texture: content::TextureHandle, pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates. /// This object's center, in world coordinates.
pub pos: Point3<f32>, pub pos: Point3<f32>,
@ -97,8 +97,8 @@ pub struct ObjectSprite {
/// A sprite that is drawn relative to an ObjectSprite. /// A sprite that is drawn relative to an ObjectSprite.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ObjectSubSprite { pub struct ObjectSubSprite {
/// The sprite texture to draw /// The sprite to draw
pub texture: content::TextureHandle, pub sprite: content::SpriteHandle,
/// This object's position, in world coordinates. /// This object's position, in world coordinates.
/// This is relative to this sprite's parent. /// This is relative to this sprite's parent.

View File

@ -1,7 +1,12 @@
use crate::content; use crate::{
content,
globaluniform::{ImageLocation, ImageLocationArray, SpriteData, SpriteDataArray},
};
use anyhow::Result; use anyhow::Result;
use bytemuck::Zeroable;
use galactica_packer::SpriteAtlasImage;
use image::GenericImageView; use image::GenericImageView;
use std::{collections::HashMap, fs::File, io::Read, num::NonZeroU32}; use std::{fs::File, io::Read, num::NonZeroU32};
use wgpu::BindGroupLayout; use wgpu::BindGroupLayout;
pub(crate) struct RawTexture { pub(crate) struct RawTexture {
@ -67,57 +72,60 @@ impl RawTexture {
} }
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone)]
pub struct Texture { pub struct Texture {
pub index: u32, // Index in texture array pub index: u32, // Index in texture array
pub len: u32, // Number of frames pub len: u32, // Number of frames
pub fps: f32, // Frames per second pub fps: f32, // Frames per second
pub aspect: f32, // width / height pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture pub repeat: u32, // How to re-play this texture
pub location: Vec<SpriteAtlasImage>,
} }
pub struct TextureArray { pub struct TextureArray {
pub bind_group: wgpu::BindGroup, pub bind_group: wgpu::BindGroup,
pub bind_group_layout: BindGroupLayout, pub bind_group_layout: BindGroupLayout,
starfield_handle: content::TextureHandle, pub image_locations: ImageLocationArray,
textures: HashMap<content::TextureHandle, Texture>, pub sprite_data: SpriteDataArray,
} }
impl TextureArray { impl TextureArray {
pub fn get_starfield_texture(&self) -> Texture {
*self.textures.get(&self.starfield_handle).unwrap()
}
pub fn get_texture(&self, handle: content::TextureHandle) -> Texture {
match self.textures.get(&handle) {
Some(x) => *x,
None => unreachable!("Tried to get a texture that doesn't exist"),
}
}
pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, ct: &content::Content) -> Result<Self> { pub fn new(device: &wgpu::Device, queue: &wgpu::Queue, ct: &content::Content) -> Result<Self> {
// Load all textures // Load all textures
let mut texture_data = Vec::new(); let mut texture_data = Vec::new();
let mut textures = HashMap::new();
for t in &ct.textures { println!("opening image");
let index = texture_data.len() as u32; let mut f = File::open("atlas-0.bmp")?;
for f in &t.frames { let mut bytes = Vec::new();
let mut f = File::open(&f)?; f.read_to_end(&mut bytes)?;
let mut bytes = Vec::new(); texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, "Atlas")?);
f.read_to_end(&mut bytes)?;
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?); let mut image_locations = ImageLocationArray::zeroed();
let mut sprite_data = SpriteDataArray::zeroed();
println!("sending to gpu");
let mut image_counter = 0;
for t in &ct.sprites {
sprite_data.data[image_counter as usize] = SpriteData {
frame_count: t.frames.len() as u32,
repeatmode: t.repeat.as_int(),
aspect: t.aspect,
fps: t.fps,
first_frame: image_counter,
_padding: Default::default(),
};
// Insert texture location data
for path in &t.frames {
let image = ct.get_image(&path);
image_locations.data[image_counter as usize] = ImageLocation {
xpos: image.x,
ypos: image.y,
width: image.w,
height: image.h,
};
image_counter += 1;
} }
textures.insert(
t.handle,
Texture {
index,
aspect: t.handle.aspect,
fps: t.fps,
len: t.frames.len() as u32,
repeat: t.repeat.as_int(),
},
);
} }
let sampler = device.create_sampler(&wgpu::SamplerDescriptor { let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
@ -136,7 +144,7 @@ impl TextureArray {
// Texture data // Texture data
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
binding: 0, binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Texture { ty: wgpu::BindingType::Texture {
multisampled: false, multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2, view_dimension: wgpu::TextureViewDimension::D2,
@ -147,7 +155,7 @@ impl TextureArray {
// Texture sampler // Texture sampler
wgpu::BindGroupLayoutEntry { wgpu::BindGroupLayoutEntry {
binding: 1, binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT, visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: NonZeroU32::new(1), count: NonZeroU32::new(1),
}, },
@ -161,7 +169,6 @@ impl TextureArray {
entries: &[ entries: &[
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
binding: 0, binding: 0,
// Array of all views
resource: wgpu::BindingResource::TextureViewArray(&views), resource: wgpu::BindingResource::TextureViewArray(&views),
}, },
wgpu::BindGroupEntry { wgpu::BindGroupEntry {
@ -174,8 +181,8 @@ impl TextureArray {
return Ok(Self { return Ok(Self {
bind_group, bind_group,
bind_group_layout, bind_group_layout,
textures: textures, image_locations,
starfield_handle: ct.get_starfield_handle(), sprite_data,
}); });
} }
} }

View File

@ -89,7 +89,7 @@ pub struct ObjectInstance {
pub transform: [[f32; 4]; 4], pub transform: [[f32; 4]; 4],
/// What texture to use for this sprite /// What texture to use for this sprite
pub texture_index: u32, pub sprite_index: u32,
} }
impl BufferObject for ObjectInstance { impl BufferObject for ObjectInstance {
@ -122,7 +122,7 @@ impl BufferObject for ObjectInstance {
shader_location: 5, shader_location: 5,
format: wgpu::VertexFormat::Float32x4, format: wgpu::VertexFormat::Float32x4,
}, },
// Texture // Sprite
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 16]>() as wgpu::BufferAddress,
shader_location: 6, shader_location: 6,
@ -146,7 +146,7 @@ pub struct UiInstance {
pub color: [f32; 4], pub color: [f32; 4],
/// What texture to use for this sprite /// What texture to use for this sprite
pub texture_index: u32, pub sprite_index: u32,
} }
impl BufferObject for UiInstance { impl BufferObject for UiInstance {
@ -185,7 +185,7 @@ impl BufferObject for UiInstance {
shader_location: 6, shader_location: 6,
format: wgpu::VertexFormat::Float32x4, format: wgpu::VertexFormat::Float32x4,
}, },
// Texture // Sprite
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 20]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 20]>() as wgpu::BufferAddress,
shader_location: 7, shader_location: 7,
@ -219,9 +219,8 @@ pub struct ParticleInstance {
/// Time is kept by a variable in the global uniform. /// Time is kept by a variable in the global uniform.
pub expires: f32, pub expires: f32,
/// What texture to use for this particle /// What sprite to use for this particle
pub texture_index_len_rep: [u32; 3], pub sprite_index: u32,
pub texture_aspect_fps: [f32; 2],
} }
impl BufferObject for ParticleInstance { impl BufferObject for ParticleInstance {
@ -271,17 +270,11 @@ impl BufferObject for ParticleInstance {
shader_location: 8, shader_location: 8,
format: wgpu::VertexFormat::Float32, format: wgpu::VertexFormat::Float32,
}, },
// Texture index / len / repeat // Sprite
wgpu::VertexAttribute { wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress,
shader_location: 9, shader_location: 9,
format: wgpu::VertexFormat::Uint32x3, format: wgpu::VertexFormat::Uint32,
},
// Texture aspect / fps
wgpu::VertexAttribute {
offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress,
shader_location: 10,
format: wgpu::VertexFormat::Float32x2,
}, },
], ],
} }

View File

@ -24,12 +24,12 @@ pub fn build_radar(
let (_, player_body) = physics.get_ship_body(player).unwrap(); let (_, player_body) = physics.get_ship_body(player).unwrap();
let player_position = util::rigidbody_position(player_body); let player_position = util::rigidbody_position(player_body);
let planet_texture = ct.get_texture_handle("ui::planetblip"); let planet_sprite = ct.get_sprite_handle("ui::planetblip");
let ship_texture = ct.get_texture_handle("ui::shipblip"); let ship_sprite = ct.get_sprite_handle("ui::shipblip");
let arrow_texture = ct.get_texture_handle("ui::centerarrow"); let arrow_sprite = ct.get_sprite_handle("ui::centerarrow");
out.push(UiSprite { out.push(UiSprite {
texture: ct.get_texture_handle("ui::radar"), sprite: ct.get_sprite_handle("ui::radar"),
pos: AnchoredUiPosition::NwNw(Point2 { x: 10.0, y: -10.0 }), pos: AnchoredUiPosition::NwNw(Point2 { x: 10.0, y: -10.0 }),
dimensions: Point2 { dimensions: Point2 {
x: radar_size, x: radar_size,
@ -57,7 +57,7 @@ pub fn build_radar(
continue; continue;
} }
out.push(UiSprite { out.push(UiSprite {
texture: planet_texture, sprite: planet_sprite,
pos: AnchoredUiPosition::NwC( pos: AnchoredUiPosition::NwC(
Point2 { Point2 {
x: radar_size / 2.0 + 10.0, x: radar_size / 2.0 + 10.0,
@ -65,7 +65,7 @@ pub fn build_radar(
} + (d * (radar_size / 2.0)), } + (d * (radar_size / 2.0)),
), ),
dimensions: Point2 { dimensions: Point2 {
x: planet_texture.aspect, x: planet_sprite.aspect,
y: 1.0, y: 1.0,
} * size, } * size,
angle: o.angle, angle: o.angle,
@ -77,7 +77,7 @@ pub fn build_radar(
// Draw ships // Draw ships
for (s, r) in physics.iter_ship_body() { for (s, r) in physics.iter_ship_body() {
let ship = ct.get_ship(s.ship.handle); let ship = ct.get_ship(s.ship.handle);
let size = (ship.size * ship.sprite_texture.aspect) * ship_scale; let size = (ship.size * ship.sprite.aspect) * ship_scale;
let p = util::rigidbody_position(r); let p = util::rigidbody_position(r);
let d = (p - player_position) / radar_range; let d = (p - player_position) / radar_range;
let m = d.magnitude() + (size / (2.0 * radar_size)); let m = d.magnitude() + (size / (2.0 * radar_size));
@ -92,7 +92,7 @@ pub fn build_radar(
let f = ct.get_faction(s.ship.faction).color; let f = ct.get_faction(s.ship.faction).color;
let f = [f[0], f[1], f[2], 1.0]; let f = [f[0], f[1], f[2], 1.0];
out.push(UiSprite { out.push(UiSprite {
texture: ship_texture, sprite: ship_sprite,
pos: AnchoredUiPosition::NwC( pos: AnchoredUiPosition::NwC(
Point2 { Point2 {
x: radar_size / 2.0 + 10.0, x: radar_size / 2.0 + 10.0,
@ -100,7 +100,7 @@ pub fn build_radar(
} + (d * (radar_size / 2.0)), } + (d * (radar_size / 2.0)),
), ),
dimensions: Point2 { dimensions: Point2 {
x: ship_texture.aspect, x: ship_sprite.aspect,
y: 1.0, y: 1.0,
} * size, } * size,
angle: -angle, angle: -angle,
@ -118,13 +118,13 @@ pub fn build_radar(
let d = d * (radar_size / 2.0); let d = d * (radar_size / 2.0);
let color = Some([0.3, 0.3, 0.3, 1.0]); let color = Some([0.3, 0.3, 0.3, 1.0]);
if m < 0.8 { if m < 0.8 {
let texture = ct.get_texture_handle("ui::radarframe"); let sprite = ct.get_sprite_handle("ui::radarframe");
let dimensions = Point2 { let dimensions = Point2 {
x: texture.aspect, x: sprite.aspect,
y: 1.0, y: 1.0,
} * 7.0f32.min((0.8 - m) * 70.0); } * 7.0f32.min((0.8 - m) * 70.0);
out.push(UiSprite { out.push(UiSprite {
texture, sprite,
pos: AnchoredUiPosition::NwNw(Point2 { pos: AnchoredUiPosition::NwNw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x, x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) + d.y, y: (radar_size / -2.0 - 10.0) + d.y,
@ -135,7 +135,7 @@ pub fn build_radar(
}); });
out.push(UiSprite { out.push(UiSprite {
texture, sprite,
pos: AnchoredUiPosition::NwSw(Point2 { pos: AnchoredUiPosition::NwSw(Point2 {
x: (radar_size / 2.0 + 10.0) - d.x, x: (radar_size / 2.0 + 10.0) - d.x,
y: (radar_size / -2.0 - 10.0) - d.y, y: (radar_size / -2.0 - 10.0) - d.y,
@ -146,7 +146,7 @@ pub fn build_radar(
}); });
out.push(UiSprite { out.push(UiSprite {
texture, sprite,
pos: AnchoredUiPosition::NwSe(Point2 { pos: AnchoredUiPosition::NwSe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x, x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) - d.y, y: (radar_size / -2.0 - 10.0) - d.y,
@ -157,7 +157,7 @@ pub fn build_radar(
}); });
out.push(UiSprite { out.push(UiSprite {
texture, sprite,
pos: AnchoredUiPosition::NwNe(Point2 { pos: AnchoredUiPosition::NwNe(Point2 {
x: (radar_size / 2.0 + 10.0) + d.x, x: (radar_size / 2.0 + 10.0) + d.x,
y: (radar_size / -2.0 - 10.0) + d.y, y: (radar_size / -2.0 - 10.0) + d.y,
@ -174,7 +174,7 @@ pub fn build_radar(
if m > 200.0 { if m > 200.0 {
let player_angle: Deg<f32> = q.angle(Vector2 { x: 0.0, y: 1.0 }).into(); let player_angle: Deg<f32> = q.angle(Vector2 { x: 0.0, y: 1.0 }).into();
out.push(UiSprite { out.push(UiSprite {
texture: arrow_texture, sprite: arrow_sprite,
pos: AnchoredUiPosition::NwC( pos: AnchoredUiPosition::NwC(
Point2 { Point2 {
x: radar_size / 2.0 + 10.0, x: radar_size / 2.0 + 10.0,
@ -182,7 +182,7 @@ pub fn build_radar(
} + ((q.normalize() * 0.865) * (radar_size / 2.0)), } + ((q.normalize() * 0.865) * (radar_size / 2.0)),
), ),
dimensions: Point2 { dimensions: Point2 {
x: arrow_texture.aspect, x: arrow_sprite.aspect,
y: 1.0, y: 1.0,
} * 10.0, } * 10.0,
angle: -player_angle, angle: -player_angle,

View File

@ -44,7 +44,7 @@ impl ProjectileWorldObject {
let ang: Deg<f32> = rot.angle(Vector2 { x: 1.0, y: 0.0 }).into(); let ang: Deg<f32> = rot.angle(Vector2 { x: 1.0, y: 0.0 }).into();
ObjectSprite { ObjectSprite {
texture: self.projectile.content.sprite_texture, sprite: self.projectile.content.sprite,
pos: Point3 { pos: Point3 {
x: pos.x, x: pos.x,
y: pos.y, y: pos.y,

View File

@ -86,7 +86,7 @@ impl ShipWorldObject {
ObjectSprite { ObjectSprite {
pos: (ship_pos.x, ship_pos.y, 1.0).into(), pos: (ship_pos.x, ship_pos.y, 1.0).into(),
texture: s.sprite_texture, sprite: s.sprite,
angle: -ship_ang, angle: -ship_ang,
size: s.size, size: s.size,

View File

@ -189,7 +189,7 @@ impl<'a> World {
} }
}; };
particles.push(ParticleBuilder { particles.push(ParticleBuilder {
texture: x.texture, sprite: x.sprite,
pos: Point2 { x: pos.x, y: pos.y }, pos: Point2 { x: pos.x, y: pos.y },
velocity, velocity,
angle: -angle, angle: -angle,
@ -332,7 +332,7 @@ impl<'a> World {
content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(&pr), content::ImpactInheritVelocity::Projectile => util::rigidbody_velocity(&pr),
}; };
particles.push(ParticleBuilder { particles.push(ParticleBuilder {
texture: x.texture, sprite: x.sprite,
pos: Point2 { x: pos.x, y: pos.y }, pos: Point2 { x: pos.x, y: pos.y },
velocity, velocity,
angle: -angle, angle: -angle,