Compare commits
No commits in common. "1001b8ba4a38d65cd21803f5cfc76209d2fa27bd" and "e344c344ad0678315fef5623154c1ea99caa7d95" have entirely different histories.
1001b8ba4a
...
e344c344ad
|
@ -610,7 +610,6 @@ version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cgmath",
|
"cgmath",
|
||||||
"galactica-packer",
|
|
||||||
"image",
|
"image",
|
||||||
"nalgebra",
|
"nalgebra",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -628,18 +627,6 @@ dependencies = [
|
||||||
"rand",
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "galactica-packer"
|
|
||||||
version = "0.0.0"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"galactica-constants",
|
|
||||||
"image",
|
|
||||||
"serde",
|
|
||||||
"toml",
|
|
||||||
"walkdir",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "galactica-render"
|
name = "galactica-render"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
@ -649,7 +636,6 @@ dependencies = [
|
||||||
"cgmath",
|
"cgmath",
|
||||||
"galactica-constants",
|
"galactica-constants",
|
||||||
"galactica-content",
|
"galactica-content",
|
||||||
"galactica-packer",
|
|
||||||
"image",
|
"image",
|
||||||
"rand",
|
"rand",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
|
|
|
@ -49,7 +49,6 @@ galactica-world = { path = "crates/world" }
|
||||||
galactica-behavior = { path = "crates/behavior" }
|
galactica-behavior = { path = "crates/behavior" }
|
||||||
galactica-gameobject = { path = "crates/gameobject" }
|
galactica-gameobject = { path = "crates/gameobject" }
|
||||||
galactica-ui = { path = "crates/ui" }
|
galactica-ui = { path = "crates/ui" }
|
||||||
galactica-packer = { path = "crates/packer" }
|
|
||||||
galactica = { path = "crates/galactica" }
|
galactica = { path = "crates/galactica" }
|
||||||
|
|
||||||
image = { version = "0.24", features = ["png"] }
|
image = { version = "0.24", features = ["png"] }
|
||||||
|
|
4
TODO.md
4
TODO.md
|
@ -80,7 +80,6 @@
|
||||||
- Engine flares shouldn't be centered
|
- Engine flares shouldn't be centered
|
||||||
- Sprite optimization: do we need to allocate a new `Vec` every frame? Probably not.
|
- Sprite optimization: do we need to allocate a new `Vec` every frame? Probably not.
|
||||||
- Better error when run outside of directory
|
- Better error when run outside of directory
|
||||||
- Documentation site & front page
|
|
||||||
|
|
||||||
## Content
|
## Content
|
||||||
- Angled engines
|
- Angled engines
|
||||||
|
@ -118,7 +117,7 @@
|
||||||
- Nova dust parallax
|
- Nova dust parallax
|
||||||
- Ship outlines in radar
|
- Ship outlines in radar
|
||||||
- Engine flare ease in/out
|
- Engine flare ease in/out
|
||||||
- Lens flare
|
|
||||||
|
|
||||||
## Write and Document
|
## Write and Document
|
||||||
- Parallax
|
- Parallax
|
||||||
|
@ -133,7 +132,6 @@
|
||||||
- Ship AI
|
- Ship AI
|
||||||
- Handles
|
- Handles
|
||||||
- Content specification and pipeline
|
- Content specification and pipeline
|
||||||
- How packer and optimizations work, and why
|
|
||||||
|
|
||||||
|
|
||||||
## Ideas
|
## Ideas
|
||||||
|
|
|
@ -38,24 +38,8 @@ file = "ui/center-arrow.png"
|
||||||
duration = 0.15
|
duration = 0.15
|
||||||
repeat = "once"
|
repeat = "once"
|
||||||
frames = [
|
frames = [
|
||||||
"particle/blaster/01.png",
|
"particle/blaster-01.png",
|
||||||
"particle/blaster/02.png",
|
"particle/blaster-02.png",
|
||||||
"particle/blaster/03.png",
|
"particle/blaster-03.png",
|
||||||
"particle/blaster/04.png",
|
"particle/blaster-04.png",
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
[texture."particle::explosion"]
|
|
||||||
duration = 0.4
|
|
||||||
repeat = "once"
|
|
||||||
frames = [
|
|
||||||
"particle/explosion-large/01.png",
|
|
||||||
"particle/explosion-large/02.png",
|
|
||||||
"particle/explosion-large/03.png",
|
|
||||||
"particle/explosion-large/04.png",
|
|
||||||
"particle/explosion-large/05.png",
|
|
||||||
"particle/explosion-large/06.png",
|
|
||||||
"particle/explosion-large/07.png",
|
|
||||||
"particle/explosion-large/08.png",
|
|
||||||
"particle/explosion-large/09.png",
|
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,7 +18,6 @@ fn main() -> Result<()> {
|
||||||
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::TEXTURE_ROOT),
|
||||||
PathBuf::from("spriteatlas.toml"),
|
|
||||||
galactica_constants::STARFIELD_TEXTURE_NAME.to_owned(),
|
galactica_constants::STARFIELD_TEXTURE_NAME.to_owned(),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
[[bin]]
|
|
||||||
name = "galactica-packer"
|
|
||||||
path = "src/main.rs"
|
|
||||||
|
|
||||||
[package]
|
|
||||||
name = "galactica-packer"
|
|
||||||
description = "Galactica's sprite packer"
|
|
||||||
categories = { workspace = true }
|
|
||||||
keywords = { workspace = true }
|
|
||||||
version = { workspace = true }
|
|
||||||
rust-version = { workspace = true }
|
|
||||||
authors = { workspace = true }
|
|
||||||
edition = { workspace = true }
|
|
||||||
homepage = { workspace = true }
|
|
||||||
repository = { workspace = true }
|
|
||||||
license = { workspace = true }
|
|
||||||
documentation = { workspace = true }
|
|
||||||
readme = { workspace = true }
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
galactica-constants = { workspace = true }
|
|
||||||
|
|
||||||
image = { workspace = true }
|
|
||||||
toml = { workspace = true }
|
|
||||||
serde = { workspace = true }
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
walkdir = { workspace = true }
|
|
|
@ -1,201 +0,0 @@
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use galactica_packer::{SpriteAtlasImage, SpriteAtlasIndex};
|
|
||||||
use image::{imageops, ImageBuffer, Rgba, RgbaImage};
|
|
||||||
use std::{
|
|
||||||
fs::File,
|
|
||||||
io::{Read, Write},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: article
|
|
||||||
// TODO: rework texturearray
|
|
||||||
// TODO: reasonable sprite sizes
|
|
||||||
// TODO: consistent naming
|
|
||||||
// spriteatlas: the big images
|
|
||||||
// texture: the same, what we load to wgpu
|
|
||||||
// image: a single file
|
|
||||||
// sprite: a possibly animated texture
|
|
||||||
|
|
||||||
pub struct AtlasSet {
|
|
||||||
/// The width of each atlas
|
|
||||||
texture_width: u32,
|
|
||||||
|
|
||||||
/// The height of each atlas
|
|
||||||
texture_height: u32,
|
|
||||||
|
|
||||||
/// Make at most this many atlases
|
|
||||||
/// Probably determined by the GPU
|
|
||||||
texture_limit: usize,
|
|
||||||
|
|
||||||
/// Keeps track of image files
|
|
||||||
index: SpriteAtlasIndex,
|
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
|
|
||||||
/// A list of used regions in each texture
|
|
||||||
/// Format: ([xpos, ypos], [width, height])
|
|
||||||
/// in pixels, with (0, 0) at top left
|
|
||||||
used_regions: Vec<Vec<([u32; 2], [u32; 2])>>,
|
|
||||||
|
|
||||||
/// Used to calculate packing efficiency
|
|
||||||
used_area: f64,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AtlasSet {
|
|
||||||
pub fn new(texture_width: u32, texture_height: u32, texture_limit: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
texture_width,
|
|
||||||
texture_height,
|
|
||||||
texture_limit,
|
|
||||||
texture_list: Vec::new(),
|
|
||||||
image_max_sizes: Vec::new(),
|
|
||||||
used_regions: Vec::new(),
|
|
||||||
index: SpriteAtlasIndex::new(),
|
|
||||||
used_area: 0f64,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !used {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We found a spot for this image, write it.
|
|
||||||
let x = pixel_idx % self.texture_width;
|
|
||||||
let y = pixel_idx / self.texture_height;
|
|
||||||
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(),
|
|
||||||
SpriteAtlasImage {
|
|
||||||
atlas: atlas_idx,
|
|
||||||
x: x as f32 / self.texture_width as f32,
|
|
||||||
y: y as f32 / self.texture_height as f32,
|
|
||||||
w: dim[0] as f32 / self.texture_width as f32,
|
|
||||||
h: dim[1] as f32 / self.texture_height as f32,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return Ok(atlas_idx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_files<F>(self, atlas_path: F, index_path: &Path) -> Result<()>
|
|
||||||
where
|
|
||||||
F: Fn(usize) -> PathBuf,
|
|
||||||
{
|
|
||||||
// Save atlases
|
|
||||||
for i in 0..self.texture_list.len() {
|
|
||||||
self.texture_list[i].save(atlas_path(i))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save index
|
|
||||||
let toml = toml::to_string(&self.index)?;
|
|
||||||
let mut f = File::create(index_path)?;
|
|
||||||
f.write_all(toml.as_bytes())?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
pub fn get_width(&self) -> u32 {
|
|
||||||
self.texture_width
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_height(&self) -> u32 {
|
|
||||||
self.texture_height
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_limit(&self) -> usize {
|
|
||||||
self.texture_limit
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_used_area(&self) -> f64 {
|
|
||||||
self.used_area
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn num_textures(&self) -> usize {
|
|
||||||
self.texture_list.len()
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
pub fn get_efficiency(&self) -> f64 {
|
|
||||||
self.used_area
|
|
||||||
/ (self.texture_height as f64
|
|
||||||
* self.texture_width as f64
|
|
||||||
* self.texture_list.len() as f64)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
#![warn(missing_docs)]
|
|
||||||
|
|
||||||
//! 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 serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
/// The location of a single image in a sprite atlas
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
pub struct SpriteAtlasImage {
|
|
||||||
/// The index of the atlas this image is in
|
|
||||||
pub atlas: usize,
|
|
||||||
|
|
||||||
/// x-position of this image
|
|
||||||
/// (between 0 and 1, using wgpu texture coordinates)
|
|
||||||
pub x: f32,
|
|
||||||
|
|
||||||
/// y-position of this image
|
|
||||||
/// (between 0 and 1, using wgpu texture coordinates)
|
|
||||||
pub y: f32,
|
|
||||||
|
|
||||||
/// Width of this image
|
|
||||||
/// (between 0 and 1, using wgpu texture coordinates)
|
|
||||||
pub w: f32,
|
|
||||||
|
|
||||||
/// Height of this image
|
|
||||||
/// (between 0 and 1, using wgpu texture coordinates)
|
|
||||||
pub h: f32,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SpriteAtlasIndex {
|
|
||||||
/// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,86 +0,0 @@
|
||||||
mod atlasset;
|
|
||||||
|
|
||||||
use atlasset::AtlasSet;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use image::io::Reader;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
// TODO: procedural sun coloring
|
|
||||||
// TODO: transparency buffer
|
|
||||||
// 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())
|
|
||||||
{
|
|
||||||
if e.metadata().unwrap().is_file() {
|
|
||||||
// TODO: better warnings
|
|
||||||
match e.path().extension() {
|
|
||||||
Some(t) => {
|
|
||||||
if t.to_str() != Some("png") {
|
|
||||||
println!("[WARNING] {e:#?} is not a png file, skipping.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
println!("[WARNING] {e:#?} is not a png file, skipping.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = e.path().to_path_buf();
|
|
||||||
let reader = Reader::open(&path)?;
|
|
||||||
let dim = reader.into_dimensions()?;
|
|
||||||
files.push((path, [dim.0, dim.1]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by image size
|
|
||||||
files.sort_by(|a, b| {
|
|
||||||
let a = a.1[0] * a.1[1];
|
|
||||||
let b = b.1[0] * b.1[1];
|
|
||||||
b.cmp(&a)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create atlas set
|
|
||||||
let mut atlas_set = AtlasSet::new(8192, 8192, 16);
|
|
||||||
let total = files.len();
|
|
||||||
let mut i = 0;
|
|
||||||
for (path, dim) in files {
|
|
||||||
i += 1;
|
|
||||||
let atlas_idx = atlas_set.write_image(&path, dim)?;
|
|
||||||
println!(
|
|
||||||
"({i} of {total}, atlas {atlas_idx}, efficiency {:.02}%) Added {}",
|
|
||||||
100.0 * atlas_set.get_efficiency(),
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"Packing efficiency: {:.02}%",
|
|
||||||
100.0 * atlas_set.get_efficiency()
|
|
||||||
);
|
|
||||||
|
|
||||||
println!("Saving files...");
|
|
||||||
atlas_set.save_files(
|
|
||||||
|x| PathBuf::from(format!("atlas-{x:0.2}.png")),
|
|
||||||
&PathBuf::from("spriteatlas.toml"),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
galactica-content = { workspace = true }
|
galactica-content = { workspace = true }
|
||||||
galactica-constants = { workspace = true }
|
galactica-constants = { workspace = true }
|
||||||
galactica-packer = { workspace = true }
|
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
cgmath = { workspace = true }
|
cgmath = { workspace = true }
|
||||||
|
|
Loading…
Reference in New Issue