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
- Trade
- Missions
- Procedural suns
## Camera
- Shake/wobble on heavy hits?
@ -134,6 +135,8 @@
- Handles
- Content specification and pipeline
- How packer and optimizations work, and why
- How big should sprites be? (resize existing)
- Naming: atlas, sprite, image, frame, texture
## 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)
rate_rng = 0.1
# TODO: apply force on fire
projectile.sprite_texture = "projectile::blaster"
# TODO: apply force to ship on fire
projectile.sprite = "projectile::blaster"
# Height of projectile in game units
projectile.size = 6
projectile.size_rng = 0.0
@ -29,13 +29,13 @@ projectile.force = 0.0
projectile.collider.ball.radius = 2.0
projectile.impact.texture = "particle::blaster"
projectile.impact.sprite = "particle::explosion"
projectile.impact.lifetime = "inherit"
projectile.impact.inherit_velocity = "target"
projectile.impact.size = 3.0
projectile.expire.texture = "particle::blaster"
projectile.expire.sprite = "particle::blaster"
projectile.expire.lifetime = "inherit"
projectile.expire.inherit_velocity = "projectile"
projectile.expire.size = 3.0

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
[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.size = 2000
object.earth.sprite_texture = "planet::earth"
object.earth.sprite = "planet::earth"
object.earth.position.center = "star"
object.earth.position.radius = 4000
object.earth.position.angle = 0
object.earth.position.z = 10.0
object.earth.size = 1000
object.luna.sprite_texture = "planet::luna"
object.luna.sprite = "planet::luna"
object.luna.position.center = "earth"
object.luna.position.radius = 1600
object.luna.position.angle = 135

View File

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

View File

@ -17,6 +17,8 @@ readme = { workspace = true }
workspace = true
[dependencies]
galactica-packer = { workspace = true }
serde = { workspace = true }
toml = { 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.
use std::{cmp::Eq, hash::Hash};
/// A lightweight representation of a
/// A lightweight representation of a sprite
#[derive(Debug, Clone, Copy)]
pub struct TextureHandle {
/// The index of this texture in content.textures
pub(crate) index: usize,
pub struct SpriteHandle {
/// The index of this sprite in content.sprites
/// 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,
}
impl Hash for TextureHandle {
impl Hash for SpriteHandle {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.index.hash(state)
}
}
impl Eq for TextureHandle {}
impl PartialEq for TextureHandle {
impl Eq for SpriteHandle {}
impl PartialEq for SpriteHandle {
fn eq(&self, other: &Self) -> bool {
self.index.eq(&other.index)
}

View File

@ -8,6 +8,7 @@ mod part;
mod util;
use anyhow::{Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use std::{
collections::HashMap,
fs::File,
@ -17,10 +18,10 @@ use std::{
use toml;
use walkdir::WalkDir;
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SystemHandle, TextureHandle};
pub use handle::{FactionHandle, GunHandle, OutfitHandle, ShipHandle, SpriteHandle, SystemHandle};
pub use part::{
EnginePoint, Faction, Gun, GunPoint, ImpactInheritVelocity, Outfit, OutfitSpace, Projectile,
ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, System, Texture,
ProjectileCollider, ProjectileParticle, Relationship, RepeatMode, Ship, Sprite, System,
};
mod syntax {
@ -28,7 +29,7 @@ mod syntax {
use serde::Deserialize;
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)]
pub struct Root {
@ -36,7 +37,7 @@ mod syntax {
pub ship: Option<HashMap<String, ship::syntax::Ship>>,
pub system: Option<HashMap<String, system::syntax::System>>,
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>>,
}
@ -72,7 +73,7 @@ mod syntax {
ship: None,
system: None,
outfit: None,
texture: None,
sprite: None,
faction: None,
}
}
@ -84,8 +85,8 @@ mod syntax {
.with_context(|| "while merging systems")?;
merge_hashmap(&mut self.outfit, other.outfit)
.with_context(|| "while merging outfits")?;
merge_hashmap(&mut self.texture, other.texture)
.with_context(|| "while merging textures")?;
merge_hashmap(&mut self.sprite, other.sprite)
.with_context(|| "while merging sprites")?;
merge_hashmap(&mut self.faction, other.faction)
.with_context(|| "while merging factions")?;
return Ok(());
@ -94,10 +95,10 @@ mod syntax {
}
trait Build {
type InputSyntax;
type InputSyntaxType;
/// 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
Self: Sized;
}
@ -106,18 +107,21 @@ trait Build {
#[derive(Debug)]
pub struct Content {
/* Configuration values */
/// Root directory for textures
texture_root: PathBuf,
/// Name of starfield texture
starfield_texture_name: String,
/// Root directory for image
image_root: PathBuf,
/// Name of starfield sprite
starfield_sprite_name: String,
/// Textures
pub textures: Vec<part::texture::Texture>,
/// Sprites
pub sprites: Vec<part::sprite::Sprite>,
/// Map strings to texture names.
/// 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
starfield_handle: Option<handle::TextureHandle>,
starfield_handle: Option<handle::SpriteHandle>,
/// Keeps track of which images are in which texture
sprite_atlas: SpriteAtlas,
/// Outfits
outfits: Vec<part::outfit::Outfit>,
@ -148,6 +152,7 @@ impl Content {
pub fn load_dir(
path: PathBuf,
texture_root: PathBuf,
atlas_index: PathBuf,
starfield_texture_name: String,
) -> Result<Self> {
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 {
sprite_atlas: atlas,
systems: Vec::new(),
ships: Vec::new(),
guns: Vec::new(),
outfits: Vec::new(),
textures: Vec::new(),
sprites: Vec::new(),
factions: Vec::new(),
texture_index: HashMap::new(),
sprite_index: HashMap::new(),
starfield_handle: None,
texture_root,
starfield_texture_name,
image_root: texture_root,
starfield_sprite_name: starfield_texture_name,
};
// Order here matters, usually
if root.texture.is_some() {
part::texture::Texture::build(root.texture.take().unwrap(), &mut content)?;
if root.sprite.is_some() {
part::sprite::Sprite::build(root.sprite.take().unwrap(), &mut content)?;
}
if root.ship.is_some() {
part::ship::Ship::build(root.ship.take().unwrap(), &mut content)?;
}
@ -216,27 +230,32 @@ impl Content {
// Access methods
impl Content {
/// Get the texture handle for the starfield texture
pub fn get_starfield_handle(&self) -> TextureHandle {
/// Get the handle for the starfield sprite
pub fn get_starfield_handle(&self) -> SpriteHandle {
match self.starfield_handle {
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
pub fn get_texture_handle(&self, name: &str) -> TextureHandle {
return match self.texture_index.get(name) {
/// Get a handle from a sprite name
pub fn get_sprite_handle(&self, name: &str) -> SpriteHandle {
return match self.sprite_index.get(name) {
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
pub fn get_texture(&self, h: TextureHandle) -> &Texture {
/// Get a sprite from a handle
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.
// The only TextureHandles that exist should be created by this crate.
return &self.textures[h.index];
// The only handles that exist should be created by this crate.
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
@ -249,7 +268,7 @@ impl Content {
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 {
return &self.ships[h.index];
}

View File

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

View File

@ -5,13 +5,13 @@ pub mod gun;
pub mod outfit;
mod shared;
pub mod ship;
pub mod sprite;
pub mod system;
pub mod texture;
pub use faction::{Faction, Relationship};
pub use gun::{Gun, ImpactInheritVelocity, Projectile, ProjectileCollider, ProjectileParticle};
pub use outfit::Outfit;
pub use shared::OutfitSpace;
pub use ship::{EnginePoint, GunPoint, Ship};
pub use sprite::{RepeatMode, Sprite};
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 crate::{handle::TextureHandle, Content, OutfitSpace};
use crate::{handle::SpriteHandle, Content, OutfitSpace};
pub(crate) mod syntax {
use crate::part::shared;
@ -20,7 +20,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct Engine {
pub thrust: f32,
pub flare_texture: String,
pub flare_sprite: String,
}
#[derive(Debug, Deserialize)]
@ -44,37 +44,37 @@ pub struct Outfit {
/// The engine flare sprite this outfit creates.
/// Its location and size is determined by a ship's
/// engine points.
pub engine_flare_texture: Option<TextureHandle>,
pub engine_flare_sprite: Option<SpriteHandle>,
/// How much space this outfit requires
pub space: OutfitSpace,
}
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 {
let mut o = Self {
name: outfit_name.clone(),
engine_thrust: 0.0,
steer_power: 0.0,
engine_flare_texture: None,
engine_flare_sprite: None,
space: OutfitSpace::from(outfit.space),
};
// Engine stats
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!(
"In outfit `{}`: texture `{}` doesn't exist",
"In outfit `{}`: flare sprite `{}` doesn't exist",
outfit_name,
engine.flare_texture
engine.flare_sprite
),
Some(t) => *t,
};
o.engine_thrust = engine.thrust;
o.engine_flare_texture = Some(th);
o.engine_flare_sprite = Some(th);
}
// Steering stats

View File

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

View File

@ -3,7 +3,7 @@ use image::io::Reader;
use serde::Deserialize;
use std::{collections::HashMap, path::PathBuf};
use crate::{handle::TextureHandle, Content};
use crate::{handle::SpriteHandle, Content};
pub(crate) mod syntax {
use serde::Deserialize;
@ -16,18 +16,18 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum Texture {
Static(StaticTexture),
Frames(FramesTexture),
pub enum Sprite {
Static(StaticSprite),
Frames(FrameSprite),
}
#[derive(Debug, Deserialize)]
pub struct StaticTexture {
pub struct StaticSprite {
pub file: PathBuf,
}
#[derive(Debug, Deserialize)]
pub struct FramesTexture {
pub struct FrameSprite {
pub frames: Vec<PathBuf>,
pub duration: f32,
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)]
pub struct Texture {
/// The name of this texture
pub struct Sprite {
/// The name of this sprite
pub name: String,
/// The handle for this texture
pub handle: TextureHandle,
/// This sprite's handle
pub handle: SpriteHandle,
/// The frames of this texture
/// (static textures have one frame)
/// The file names of frames of this sprite.
/// unanimated sprites have one frame.
pub frames: Vec<PathBuf>,
/// The speed of this texture's animation
/// (static textures have zero fps)
/// The speed of this sprite's animation.
/// unanimated sprites have zero fps.
pub fps: f32,
/// How to replay this texture's animation
/// How to replay this sprite's animation
pub repeat: RepeatMode,
/// Aspect ratio of this sprite (width / height)
pub aspect: f32,
}
impl crate::Build for Texture {
type InputSyntax = HashMap<String, syntax::Texture>;
impl crate::Build for Sprite {
type InputSyntaxType = HashMap<String, syntax::Sprite>;
fn build(texture: Self::InputSyntax, ct: &mut Content) -> Result<()> {
for (texture_name, t) in texture {
fn build(sprites: Self::InputSyntaxType, ct: &mut Content) -> Result<()> {
for (sprite_name, t) in sprites {
match t {
syntax::Texture::Static(t) => {
let file = ct.texture_root.join(t.file);
syntax::Sprite::Static(t) => {
let file = ct.image_root.join(&t.file);
let reader = Reader::open(&file).with_context(|| {
format!(
"Failed to read texture `{}` from file `{}`",
texture_name,
file.display()
"Failed to read file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
let dim = reader.into_dimensions().with_context(|| {
format!(
"Failed to get dimensions of texture `{}` from file `{}`",
texture_name,
file.display()
"Failed to get dimensions of file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
let h = TextureHandle {
index: ct.textures.len(),
let h = SpriteHandle {
index: ct.sprites.len() as u32,
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() {
ct.starfield_handle = Some(h)
} else {
// 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 {
name: texture_name,
frames: vec![file],
ct.sprites.push(Self {
name: sprite_name,
frames: vec![t.file],
fps: 0.0,
handle: h,
repeat: RepeatMode::Once,
aspect: dim.0 as f32 / dim.1 as f32,
});
}
syntax::Texture::Frames(t) => {
syntax::Sprite::Frames(t) => {
let mut dim = None;
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(|| {
format!(
"Failed to read texture `{}` from file `{}`",
texture_name,
file.display()
"Failed to read file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
let d = reader.into_dimensions().with_context(|| {
format!(
"Failed to get dimensions of texture `{}` from file `{}`",
texture_name,
file.display()
"Failed to get dimensions of file `{}` in sprite `{}`",
file.display(),
sprite_name,
)
})?;
match dim {
@ -148,37 +152,34 @@ impl crate::Build for Texture {
Some(e) => {
if d != e {
bail!(
"Failed to load frames of texture `{}`. Frames have different sizes `{}`",
texture_name,
file.display()
"Failed to load frames of sprite `{}` because frames have different sizes.",
sprite_name,
)
}
}
}
}
let dim = dim.unwrap();
let h = TextureHandle {
index: ct.textures.len(),
aspect: dim.unwrap().0 as f32 / dim.unwrap().1 as f32,
let h = SpriteHandle {
index: ct.sprites.len() as u32,
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")
}
let fps = t.duration / t.frames.len() as f32;
ct.texture_index.insert(texture_name.clone(), h);
ct.textures.push(Self {
name: texture_name,
frames: t
.frames
.into_iter()
.map(|f| ct.texture_root.join(f))
.collect(),
ct.sprite_index.insert(sprite_name.clone(), h);
ct.sprites.push(Self {
name: sprite_name,
frames: t.frames,
fps,
handle: h,
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() {
bail!(
"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 std::collections::{HashMap, HashSet};
use crate::{handle::TextureHandle, util::Polar, Content};
use crate::{handle::SpriteHandle, util::Polar, Content};
pub(crate) mod syntax {
use serde::Deserialize;
@ -17,7 +17,7 @@ pub(crate) mod syntax {
#[derive(Debug, Deserialize)]
pub struct Object {
pub sprite_texture: String,
pub sprite: String,
pub position: Position,
pub size: f32,
@ -94,7 +94,7 @@ pub struct System {
#[derive(Debug)]
pub struct Object {
/// This object's sprite
pub sprite_texture: TextureHandle,
pub sprite: SpriteHandle,
/// This object's size.
/// Measured as height in game units.
@ -175,9 +175,9 @@ fn resolve_position(
}
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 {
let mut objects = Vec::new();
@ -185,17 +185,17 @@ impl crate::Build for System {
let mut cycle_detector = HashSet::new();
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!(
"In system `{}`: texture `{}` doesn't exist",
"In system `{}`: sprite `{}` doesn't exist",
system_name,
obj.sprite_texture
obj.sprite
),
Some(t) => *t,
};
objects.push(Object {
sprite_texture: th,
sprite: handle,
position: resolve_position(&system.object, &obj, cycle_detector)
.with_context(|| format!("In object {:#?}", label))?,
size: obj.size,

View File

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

View File

@ -10,7 +10,7 @@ use galactica_behavior::{behavior, ShipBehavior};
use galactica_constants;
use galactica_content as content;
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_world::{util, ShipPhysicsHandle, World};
@ -174,14 +174,15 @@ impl Game {
self.last_update = Instant::now();
}
pub fn get_frame_state(&mut self) -> FrameState {
FrameState {
pub fn get_frame_state(&mut self) -> RenderState {
RenderState {
camera_pos: self.camera.pos,
camera_zoom: self.camera.zoom,
object_sprites: self.get_object_sprites(),
ui_sprites: self.get_ui_sprites(),
new_particles: &mut self.new_particles,
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
let content = content::Content::load_dir(
PathBuf::from(galactica_constants::CONTENT_ROOT),
PathBuf::from(galactica_constants::TEXTURE_ROOT),
PathBuf::from(galactica_constants::IMAGE_ROOT),
PathBuf::from("spriteatlas.toml"),
galactica_constants::STARFIELD_TEXTURE_NAME.to_owned(),
galactica_constants::STARFIELD_SPRITE_NAME.to_owned(),
)?;
let event_loop = EventLoop::new();
let window = WindowBuilder::new().build(&event_loop).unwrap();
let mut gpu = pollster::block_on(galactica_render::GPUState::new(window, &content))?;
gpu.init();
let mut game = game::Game::new(content);
gpu.update_starfield_buffer();

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
use anyhow::{bail, Result};
use galactica_packer::{SpriteAtlasImage, SpriteAtlasIndex};
use anyhow::{bail, Context, Result};
use galactica_packer::{SpriteAtlas, SpriteAtlasImage};
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
use std::{
fs::File,
@ -11,6 +11,7 @@ use std::{
// TODO: rework texturearray
// TODO: reasonable sprite sizes
// TODO: consistent naming
// TODO: parallelize
// spriteatlas: the big images
// texture: the same, what we load to wgpu
// image: a single file
@ -28,13 +29,16 @@ pub struct AtlasSet {
texture_limit: usize,
/// Keeps track of image files
index: SpriteAtlasIndex,
index: SpriteAtlas,
/// Array of textures, grows as needed
texture_list: Vec<ImageBuffer<Rgba<u8>, Vec<u8>>>,
/// 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
/// Format: ([xpos, ypos], [width, height])
@ -43,105 +47,137 @@ pub struct AtlasSet {
/// Used to calculate packing efficiency
used_area: f64,
/// The root directory that contains all image files.
/// Files outside this directory will not be packed.
asset_root: PathBuf,
}
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 {
asset_root: asset_root.to_path_buf(),
texture_width,
texture_height,
texture_limit,
texture_list: Vec::new(),
image_max_sizes: Vec::new(),
used_regions: Vec::new(),
index: SpriteAtlasIndex::new(),
index: SpriteAtlas::new(),
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
pub fn write_image(&mut self, path: &Path, dim: [u32; 2]) -> Result<usize> {
let mut f = File::open(&path)?;
let mut bytes = Vec::new();
f.read_to_end(&mut 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.
// Includes a few speed optimizations
loop {
let mut x = 0;
let mut y = 0;
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() {
// 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
self.texture_list
.push(RgbaImage::new(self.texture_width, self.texture_height));
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;
let y = pixel_idx / self.texture_height;
let new = ([x, y], dim);
let mut used = false;
for r in &self.used_regions[atlas_idx] {
if self.boxes_overlap(*r, new) {
// Speed boost: skip the whole box
pixel_idx += new.1[0] - 1;
used = true;
break;
// Optimization: save the smallest sprite that didn't fit in each atlas,
// and don't try to add similarly-sized sprites.
if dim[0] >= self.image_max_sizes[atlas_idx][0]
&& dim[1] >= self.image_max_sizes[atlas_idx][1]
{
continue 'outer;
}
x = 0;
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;
}
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.
let x = pixel_idx % self.texture_width;
let y = pixel_idx / self.texture_height;
//let img = RgbaImage::from_pixel(dim[0], dim[1], Rgba([0, 0, 0, 255]));
imageops::overlay(&mut self.texture_list[atlas_idx], &img, x.into(), y.into());
self.used_regions[atlas_idx].push(([x, y], dim));
self.used_area += dim[0] as f64 * dim[1] as f64;
self.index.insert(
path.to_path_buf(),
let (sy, sd) = self.image_y_start[atlas_idx];
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 {
atlas: atlas_idx,
x: x as f32 / self.texture_width as f32,

View File

@ -3,10 +3,7 @@
//! This crate creates texture atlases from an asset tree.
//! The main interface for this crate is ... TODO
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use std::{collections::HashMap, path::PathBuf};
use serde::{Deserialize, Serialize};
@ -36,26 +33,16 @@ pub struct SpriteAtlasImage {
/// A map between file paths (relative to the root asset dir)
/// and [`AtlasTexture`]s.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct SpriteAtlasIndex {
pub(crate) index: HashMap<PathBuf, SpriteAtlasImage>,
pub struct SpriteAtlas {
/// The images in this atlas
pub index: HashMap<PathBuf, SpriteAtlasImage>,
}
impl SpriteAtlasIndex {
impl SpriteAtlas {
/// Make an empty [`SpriteAtlasIndex`]
pub fn new() -> Self {
Self {
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 anyhow::Result;
use anyhow::{bail, Result};
use image::io::Reader;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
// TODO: procedural sun coloring
// TODO: transparency buffer
// TODO: warning when images have extra transparency
// TODO: don't re-encode. Direct to gpu?
// (maybe not, tiling is slow. Make it work with files first.)
// TODO: path for atlas files
// TODO: rework texturearray
// TODO: reasonable sprite sizes (especially ui, document rules)
// TODO: consistent naming
// 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<()> {
let mut files = Vec::new();
for e in WalkDir::new("./assets/render")
.into_iter()
.filter_map(|e| e.ok())
{
let asset_root = Path::new("./assets/render");
// 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() {
// TODO: better warnings
match e.path().extension() {
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.");
continue;
}
@ -46,7 +40,8 @@ fn main() -> Result<()> {
let path = e.path().to_path_buf();
let reader = Reader::open(&path)?;
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)
});
// 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
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 mut i = 0;
let mut peak_efficiency = 0f64;
for (path, dim) in files {
i += 1;
let atlas_idx = atlas_set.write_image(&path, dim)?;
@ -69,6 +73,7 @@ fn main() -> Result<()> {
100.0 * atlas_set.get_efficiency(),
path.display()
);
peak_efficiency = peak_efficiency.max(atlas_set.get_efficiency());
}
println!(
@ -76,9 +81,11 @@ fn main() -> Result<()> {
100.0 * atlas_set.get_efficiency()
);
println!("Peak efficiency: {:.02}%", 100.0 * peak_efficiency);
println!("Saving 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"),
)?;

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>,
@ -9,27 +11,12 @@ struct InstanceInput {
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) texture_coords: vec2<f32>,
}
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) texture_coords: vec2<f32>,
@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;
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;
}

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) position: vec2<f32>,
@location(3) velocity: vec2<f32>,
@ -6,8 +8,7 @@ struct InstanceInput {
@location(6) size: f32,
@location(7) created: f32,
@location(8) expires: f32,
@location(9) texture_index_len_rep: vec3<u32>,
@location(10) texture_aspect_fps: vec2<f32>,
@location(9) texture_index: u32,
};
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)
var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1)
@ -57,37 +43,51 @@ fn vertex_main(
out.texture_coords = vertex.texture_coords;
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);
return out;
}
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);
if instance.texture_index_len_rep.z == u32(1) {
if rep == u32(1) {
// Repeat
frame = u32(fmod(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y)
(age / fps),
f32(len)
));
} else {
// Once
frame = u32(min(
(age / instance.texture_aspect_fps.y),
f32(instance.texture_index_len_rep.y) - 1.0
(age / fps),
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);
var scale: f32 = instance.size / global.camera_zoom.x;
var pos: vec2<f32> = vec2(vertex.position.x, vertex.position.y);
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
);
pos = rotation * pos;

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) position: vec3<f32>,
@location(3) size: f32,
@ -12,24 +14,10 @@ struct VertexInput {
struct VertexOutput {
@builtin(position) position: vec4<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)
var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1)
@ -49,7 +37,6 @@ fn vertex_main(
) -> VertexOutput {
var out: VertexOutput;
out.texture_coords = vertex.texture_coords;
out.tint = instance.tint;
// 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;
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;
}
@ -136,7 +135,7 @@ fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
let c_del = c_bot - c_top;
return textureSampleLevel(
texture_array[global.starfield_texture.x],
texture_array[in.texture_index],
sampler_array[0],
in.texture_coords,
0.0

View File

@ -1,3 +1,5 @@
// INCLUDE: global uniform header
struct InstanceInput {
@location(2) transform_matrix_0: vec4<f32>,
@location(3) transform_matrix_1: vec4<f32>,
@ -19,22 +21,6 @@ struct VertexOutput {
@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)
var texture_array: binding_array<texture_2d<f32>>;
@group(0) @binding(1)
@ -58,9 +44,19 @@ fn vertex_main(
var out: VertexOutput;
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;
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;
}

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::{
content,
globaldata::{GlobalData, GlobalDataContent},
globaluniform::{AtlasContent, DataContent, GlobalUniform, SpriteContent},
pipeline::PipelineBuilder,
sprite::ObjectSubSprite,
starfield::Starfield,
@ -18,7 +18,7 @@ use crate::{
types::{ObjectInstance, ParticleInstance, StarfieldInstance, TexturedVertex, UiInstance},
BufferObject, VertexBuffer,
},
FrameState, ObjectSprite, UiSprite, OPENGL_TO_WGPU_MATRIX,
ObjectSprite, RenderState, UiSprite, OPENGL_TO_WGPU_MATRIX,
};
/// A high-level GPU wrapper. Consumes game state,
@ -44,7 +44,7 @@ pub struct GPUState {
starfield: Starfield,
texture_array: TextureArray,
global_data: GlobalData,
global_uniform: GlobalUniform,
vertex_buffers: VertexBuffers,
}
@ -60,6 +60,19 @@ struct VertexBuffers {
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 {
/// Make a new GPUState that draws on `window`
pub async fn new(window: Window, ct: &content::Content) -> Result<Self> {
@ -161,22 +174,26 @@ impl GPUState {
};
// Load uniforms
let global_data = GlobalData::new(&device);
let global_uniform = GlobalUniform::new(&device);
let texture_array = TextureArray::new(&device, &queue, ct)?;
// Make sure these match the indices in each shader
let bind_group_layouts = &[
&texture_array.bind_group_layout,
&global_data.bind_group_layout,
&global_uniform.bind_group_layout,
];
// Create render pipelines
let object_pipeline = PipelineBuilder::new("object", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"object.wgsl"
)))
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"object.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.object)
@ -184,11 +201,15 @@ impl GPUState {
.build();
let starfield_pipeline = PipelineBuilder::new("starfield", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"starfield.wgsl"
)))
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"starfield.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.starfield)
@ -196,11 +217,11 @@ impl GPUState {
.build();
let ui_pipeline = PipelineBuilder::new("ui", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"ui.wgsl"
)))
.set_shader(&preprocess_shader(
&include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/shaders/", "ui.wgsl")),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.ui)
@ -208,11 +229,15 @@ impl GPUState {
.build();
let particle_pipeline = PipelineBuilder::new("particle", &device)
.set_shader(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"particle.wgsl"
)))
.set_shader(&preprocess_shader(
&include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/shaders/",
"particle.wgsl"
)),
&global_uniform,
1,
))
.set_format(config.format)
.set_triangle(true)
.set_vertex_buffer(&vertex_buffers.particle)
@ -239,7 +264,7 @@ impl GPUState {
starfield,
texture_array,
global_data,
global_uniform,
vertex_buffers,
});
}
@ -267,8 +292,7 @@ impl GPUState {
/// Also handles child sprites.
fn push_object_sprite(
&self,
camera_zoom: f32,
camera_pos: Point2<f32>,
state: &RenderState,
instances: &mut Vec<ObjectInstance>,
clip_ne: Point2<f32>,
clip_sw: Point2<f32>,
@ -280,10 +304,9 @@ impl GPUState {
(Point2 {
x: s.pos.x,
y: s.pos.y,
} - camera_pos.to_vec())
} - state.camera_pos.to_vec())
/ s.pos.z
};
let texture = self.texture_array.get_texture(s.texture);
// Game dimensions of this sprite post-scale.
// Don't divide by 2, we use this later.
@ -291,7 +314,7 @@ impl GPUState {
// Width or height, whichever is larger.
// 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)
// sprites that are off the screen
@ -304,7 +327,7 @@ impl GPUState {
}
// TODO: clean up
let scale = height / camera_zoom;
let scale = height / state.camera_zoom;
// Note that our mesh starts centered at (0, 0).
// 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
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
let rotate = Matrix4::from_angle_z(s.angle);
@ -333,8 +356,8 @@ impl GPUState {
// The height of the viewport is `zoom` in game units,
// but it's 2 in screen units! (since coordinates range from -1 to 1)
let translate = Matrix4::from_translation(Vector3 {
x: pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: pos.y / (camera_zoom / 2.0),
x: pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: pos.y / (state.camera_zoom / 2.0),
z: 0.0,
});
@ -345,13 +368,13 @@ impl GPUState {
instances.push(ObjectInstance {
transform: t.into(),
texture_index: texture.index,
sprite_index: s.sprite.index,
});
// Add children
if let Some(children) = &s.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`.
fn push_object_subsprite(
&self,
camera_zoom: f32,
state: &RenderState,
instances: &mut Vec<ObjectInstance>,
s: &ObjectSubSprite,
parent_pos: Point2<f32>,
parent_angle: Deg<f32>,
) {
let texture = self.texture_array.get_texture(s.texture);
let scale = s.size / (s.pos.z * camera_zoom);
let scale = s.size / (s.pos.z * state.camera_zoom);
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 screen_aspect = Matrix4::from_nonuniform_scale(1.0 / self.window_aspect, 1.0, 1.0);
let ptranslate = Matrix4::from_translation(Vector3 {
x: parent_pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: parent_pos.y / (camera_zoom / 2.0),
x: parent_pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: parent_pos.y / (state.camera_zoom / 2.0),
z: 0.0,
});
let protate = Matrix4::from_angle_z(parent_angle);
let translate = Matrix4::from_translation(Vector3 {
x: s.pos.x / (camera_zoom / 2.0) / self.window_aspect,
y: s.pos.y / (camera_zoom / 2.0),
x: s.pos.x / (state.camera_zoom / 2.0) / self.window_aspect,
y: s.pos.y / (state.camera_zoom / 2.0),
z: 0.0,
});
@ -395,7 +417,7 @@ impl GPUState {
instances.push(ObjectInstance {
transform: t.into(),
texture_index: texture.index,
sprite_index: s.sprite.index,
});
}
@ -404,7 +426,6 @@ impl GPUState {
let logical_size: LogicalSize<f32> =
self.window_size.to_logical(self.window.scale_factor());
let texture = self.texture_array.get_texture(s.texture);
let width = s.dimensions.x;
let height = s.dimensions.y;
@ -448,7 +469,7 @@ impl GPUState {
instances.push(UiInstance {
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]),
});
}
@ -456,23 +477,16 @@ impl GPUState {
/// Make an instance for all the game's sprites
/// (Objects and UI)
/// 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();
// Game coordinates (relative to camera) of ne and sw corners of screen.
// Used to skip off-screen sprites.
let clip_ne = Point2::from((-self.window_aspect, 1.0)) * framestate.camera_zoom;
let clip_sw = 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)) * state.camera_zoom;
for s in framestate.object_sprites {
self.push_object_sprite(
framestate.camera_zoom,
framestate.camera_pos,
&mut object_instances,
clip_ne,
clip_sw,
&s,
);
for s in &state.object_sprites {
self.push_object_sprite(state, &mut object_instances, clip_ne, clip_sw, &s);
}
// Enforce sprite limit
@ -489,7 +503,7 @@ impl GPUState {
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);
}
@ -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.
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 view = output
.texture
@ -552,32 +587,32 @@ impl GPUState {
timestamp_writes: None,
});
let s = state.content.get_starfield_handle();
// Update global values
self.queue.write_buffer(
&self.global_data.buffer,
&self.global_uniform.data_buffer,
0,
bytemuck::cast_slice(&[GlobalDataContent {
camera_position: framestate.camera_pos.into(),
camera_zoom: [framestate.camera_zoom, 0.0],
bytemuck::cast_slice(&[DataContent {
camera_position: state.camera_pos.into(),
camera_zoom: [state.camera_zoom, 0.0],
camera_zoom_limits: [galactica_constants::ZOOM_MIN, galactica_constants::ZOOM_MAX],
window_size: [
self.window_size.width as f32,
self.window_size.height as f32,
],
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_size_limits: [
galactica_constants::STARFIELD_SIZE_MIN,
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
for i in framestate.new_particles.iter() {
let texture = self.texture_array.get_texture(i.texture);
for i in state.new_particles.iter() {
self.queue.write_buffer(
&self.vertex_buffers.particle.instances,
ParticleInstance::SIZE * self.vertex_buffers.particle_array_head,
@ -586,10 +621,9 @@ impl GPUState {
velocity: i.velocity.into(),
rotation: Matrix2::from_angle(i.angle).into(),
size: i.size,
texture_index_len_rep: [texture.index, texture.len, texture.repeat],
texture_aspect_fps: [texture.aspect, texture.fps],
created: framestate.current_time,
expires: framestate.current_time + i.lifetime,
sprite_index: i.sprite.index,
created: state.current_time,
expires: state.current_time + i.lifetime,
}]),
);
self.vertex_buffers.particle_array_head += 1;
@ -599,15 +633,15 @@ impl GPUState {
self.vertex_buffers.particle_array_head = 0;
}
}
framestate.new_particles.clear();
state.new_particles.clear();
// 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,
// 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(1, &self.global_data.bind_group, &[]);
render_pass.set_bind_group(1, &self.global_uniform.bind_group, &[]);
// Starfield pipeline
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.
//! (Excluding data structs, like [`ObjectSprite`])
mod framestate;
mod globaldata;
mod globaluniform;
mod gpustate;
mod pipeline;
mod renderstate;
mod sprite;
mod starfield;
mod texturearray;
mod vertexbuffer;
pub use framestate::FrameState;
use galactica_content as content;
pub use gpustate::GPUState;
pub use renderstate::RenderState;
pub use sprite::{AnchoredUiPosition, ObjectSprite, ObjectSubSprite, ParticleBuilder, UiSprite};
use cgmath::Matrix4;

View File

@ -1,9 +1,10 @@
use cgmath::Point2;
use galactica_content::Content;
use crate::{ObjectSprite, ParticleBuilder, UiSprite};
/// Bundles parameters passed to a single call to GPUState::render
pub struct FrameState<'a> {
pub struct RenderState<'a> {
/// Camera position, in world units
pub camera_pos: Point2<f32>,
@ -23,4 +24,7 @@ pub struct FrameState<'a> {
// TODO: handle overflow
/// The current time, in seconds
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
pub struct ParticleBuilder {
/// The texture to use for this particle
pub texture: content::TextureHandle,
/// The sprite to use for this particle
pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates.
pub pos: Point2<f32>,
@ -18,7 +18,7 @@ pub struct ParticleBuilder {
/// This particle's lifetime, in seconds
pub lifetime: f32,
/// The size of this sprite,
/// The size of this particle,
/// given as height in world units.
pub size: f32,
}
@ -54,8 +54,8 @@ pub enum AnchoredUiPosition {
/// A sprite that represents a ui element
#[derive(Debug, Clone)]
pub struct UiSprite {
/// The texture to use for this sprite
pub texture: content::TextureHandle,
/// The sprite to draw
pub sprite: content::SpriteHandle,
/// This object's position, in logical (dpi-adjusted) pixels
pub pos: AnchoredUiPosition,
@ -75,8 +75,8 @@ pub struct UiSprite {
/// Ships, planets, debris, etc
#[derive(Debug, Clone)]
pub struct ObjectSprite {
/// The texture to use for this sprite
pub texture: content::TextureHandle,
/// The sprite to draw
pub sprite: content::SpriteHandle,
/// This object's center, in world coordinates.
pub pos: Point3<f32>,
@ -97,8 +97,8 @@ pub struct ObjectSprite {
/// A sprite that is drawn relative to an ObjectSprite.
#[derive(Debug, Clone)]
pub struct ObjectSubSprite {
/// The sprite texture to draw
pub texture: content::TextureHandle,
/// The sprite to draw
pub sprite: content::SpriteHandle,
/// This object's position, in world coordinates.
/// 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 bytemuck::Zeroable;
use galactica_packer::SpriteAtlasImage;
use image::GenericImageView;
use std::{collections::HashMap, fs::File, io::Read, num::NonZeroU32};
use std::{fs::File, io::Read, num::NonZeroU32};
use wgpu::BindGroupLayout;
pub(crate) struct RawTexture {
@ -67,57 +72,60 @@ impl RawTexture {
}
}
#[derive(Debug, Clone, Copy)]
#[derive(Debug, Clone)]
pub struct Texture {
pub index: u32, // Index in texture array
pub len: u32, // Number of frames
pub fps: f32, // Frames per second
pub aspect: f32, // width / height
pub repeat: u32, // How to re-play this texture
pub location: Vec<SpriteAtlasImage>,
}
pub struct TextureArray {
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: BindGroupLayout,
starfield_handle: content::TextureHandle,
textures: HashMap<content::TextureHandle, Texture>,
pub image_locations: ImageLocationArray,
pub sprite_data: SpriteDataArray,
}
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> {
// Load all textures
let mut texture_data = Vec::new();
let mut textures = HashMap::new();
for t in &ct.textures {
let index = texture_data.len() as u32;
for f in &t.frames {
let mut f = File::open(&f)?;
let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?;
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, &t.name)?);
println!("opening image");
let mut f = File::open("atlas-0.bmp")?;
let mut bytes = Vec::new();
f.read_to_end(&mut bytes)?;
texture_data.push(RawTexture::from_bytes(&device, &queue, &bytes, "Atlas")?);
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 {
@ -136,7 +144,7 @@ impl TextureArray {
// Texture data
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::FRAGMENT,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Texture {
multisampled: false,
view_dimension: wgpu::TextureViewDimension::D2,
@ -147,7 +155,7 @@ impl TextureArray {
// Texture sampler
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: NonZeroU32::new(1),
},
@ -161,7 +169,6 @@ impl TextureArray {
entries: &[
wgpu::BindGroupEntry {
binding: 0,
// Array of all views
resource: wgpu::BindingResource::TextureViewArray(&views),
},
wgpu::BindGroupEntry {
@ -174,8 +181,8 @@ impl TextureArray {
return Ok(Self {
bind_group,
bind_group_layout,
textures: textures,
starfield_handle: ct.get_starfield_handle(),
image_locations,
sprite_data,
});
}
}

View File

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

View File

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

View File

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

View File

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