Implement texture atlas building

This commit is contained in:
crumblingstatue 2023-04-15 19:26:36 +02:00
parent 5ec0ad0da4
commit 45801205dc
17 changed files with 558 additions and 60 deletions

View file

@ -59,7 +59,7 @@ impl App {
Ok(Self {
rw,
should_quit: false,
game: GameState::new(args.world_name, path),
game: GameState::new(args.world_name, path, &res),
res,
sf_egui,
input: Input::default(),

View file

@ -50,16 +50,16 @@ impl GameState {
}
pub(crate) fn draw_world(&mut self, rt: &mut RenderTexture, res: &mut Res) {
self.light_sources.clear();
let mut s = Sprite::with_texture(&res.tile_atlas);
let mut s = Sprite::with_texture(&res.atlas.tex);
for_each_tile_on_screen(self.camera_offset, rt.size(), |tp, sp| {
let tile = self.world.tile_at_mut(tp, &self.worldgen);
s.set_position(sp.to_sf_vec());
if tile.bg != Tile::EMPTY {
s.set_texture_rect(self.tile_db[tile.bg].atlas_offset.to_sf_rect());
s.set_texture_rect(self.tile_db[tile.bg].tex_rect.to_sf());
rt.draw(&s);
}
if tile.mid != Tile::EMPTY {
s.set_texture_rect(self.tile_db[tile.mid].atlas_offset.to_sf_rect());
s.set_texture_rect(self.tile_db[tile.mid].tex_rect.to_sf());
if let Some(light) = self.tile_db[tile.mid].light {
let pos = ScreenVec {
x: sp.x + light.x,
@ -70,7 +70,7 @@ impl GameState {
rt.draw(&s);
}
if tile.fg != Tile::EMPTY {
s.set_texture_rect(self.tile_db[tile.fg].atlas_offset.to_sf_rect());
s.set_texture_rect(self.tile_db[tile.fg].tex_rect.to_sf());
rt.draw(&s);
}
});
@ -106,8 +106,9 @@ impl GameState {
self.ambient_light,
255,
));
let mut s = Sprite::with_texture(&res.atlas.tex);
s.set_texture_rect(res.atlas.rects["light/1"].to_sf());
for ls in &self.light_sources {
let mut s = Sprite::with_texture(&res.light_texture);
let flicker = smoothwave(self.world.ticks, 40) as f32 / 64.0;
s.set_scale((4. + flicker, 4. + flicker));
s.set_origin((128., 128.));
@ -116,9 +117,11 @@ impl GameState {
}
}
pub(crate) fn new(world_name: String, path: PathBuf) -> GameState {
pub(crate) fn new(world_name: String, path: PathBuf, res: &Res) -> GameState {
let mut spawn_point = WorldPos::SURFACE_CENTER;
spawn_point.y -= 1104;
let mut tile_db = TileDb::load_or_default();
tile_db.update_rects(&res.atlas.rects);
Self {
camera_offset: spawn_point,
world: World::new(spawn_point, &world_name, path),
@ -129,7 +132,7 @@ impl GameState {
worldgen: Worldgen::from_seed(0),
ambient_light: 0,
light_sources: Vec::new(),
tile_db: TileDb::load_or_default(),
tile_db,
}
}
}

View file

@ -8,6 +8,7 @@ mod math;
mod player;
mod res;
mod stringfmt;
mod texture_atlas;
mod tiles;
mod world;
mod worldgen;

View file

@ -1,5 +1,8 @@
use std::fmt::Debug;
use egui_inspect::derive::Inspect;
use num_traits::{Num, Signed};
use serde::{Deserialize, Serialize};
use crate::world::{TPosSc, TilePos};
@ -81,6 +84,24 @@ pub fn smoothwave<T: Num + From<u8> + PartialOrd + Copy>(input: T, max: T) -> T
}
}
#[derive(Serialize, Deserialize, Debug, Inspect, Default, Clone, Copy)]
pub struct IntRect {
pub x: i32,
pub y: i32,
pub w: i32,
pub h: i32,
}
impl IntRect {
pub(crate) fn to_sf(&self) -> sfml::graphics::Rect<i32> {
sfml::graphics::Rect::<i32> {
left: self.x,
top: self.y,
width: self.w,
height: self.h,
}
}
}
#[test]
fn test_smooth_wave() {
assert_eq!(smoothwave(0, 100), 0);

View file

@ -1,9 +1,10 @@
use sfml::{audio::Music, graphics::Texture, SfBox};
use sfml::audio::Music;
use crate::texture_atlas::AtlasBundle;
#[derive(Debug)]
pub struct Res {
pub tile_atlas: SfBox<Texture>,
pub light_texture: SfBox<Texture>,
pub atlas: AtlasBundle,
pub surf_music: Music<'static>,
pub und_music: Music<'static>,
}
@ -11,8 +12,7 @@ pub struct Res {
impl Res {
pub fn load() -> anyhow::Result<Self> {
Ok(Self {
tile_atlas: Texture::from_file("res/graphics/tiles.png")?,
light_texture: Texture::from_file("res/graphics/light2.png")?,
atlas: AtlasBundle::new()?,
surf_music: Music::from_file("res/music/music.ogg").unwrap(),
und_music: Music::from_file("res/music/cave2.ogg").unwrap(),
})

102
src/texture_atlas.rs Normal file
View file

@ -0,0 +1,102 @@
use std::{collections::HashMap, path::Path};
use crate::math::IntRect;
use sfml::{graphics::Texture, SfBox};
use texture_packer::{texture::Texture as _, TexturePacker, TexturePackerConfig};
pub type RectMap = HashMap<String, IntRect>;
#[derive(Debug)]
pub struct AtlasBundle {
pub tex: SfBox<Texture>,
// Key could be `tiles/dirt` for example, derived from folder+filename without extension
pub rects: RectMap,
}
impl AtlasBundle {
pub fn new() -> anyhow::Result<Self> {
let cfg = TexturePackerConfig {
max_width: 4096,
max_height: 4096,
allow_rotation: false,
border_padding: 0,
texture_padding: 1,
texture_extrusion: 0,
trim: true,
texture_outlines: false,
};
let mut packer = TexturePacker::new_skyline(cfg);
walk_graphics(|path| {
let img = image::open(path).unwrap();
let key = path_img_key(path);
packer.pack_own(key, img).unwrap();
dbg!(path);
});
let mut rects = HashMap::new();
let mut tex = Texture::new().unwrap();
if !tex.create(packer.width(), packer.height()) {
panic!("Failed to create texture");
}
let pixbuf = make_pix_buf(&packer);
unsafe {
tex.update_from_pixels(&pixbuf, packer.width(), packer.height(), 0, 0);
}
for (k, frame) in packer.get_frames() {
rects.insert(
k.clone(),
IntRect {
x: frame.frame.x as i32,
y: frame.frame.y as i32,
w: frame.frame.w as i32,
h: frame.frame.h as i32,
},
);
}
Ok(AtlasBundle { tex, rects })
}
}
fn make_pix_buf(packer: &TexturePacker<image::DynamicImage, String>) -> Vec<u8> {
let (w, h) = (packer.width(), packer.height());
let px_size = 4;
let mut vec = vec![0; w as usize * h as usize * px_size as usize];
dbg!(w, h);
for y in 0..h {
for x in 0..w {
let idx = ((y * w + x) * px_size) as usize;
if let Some(px) = packer.get(x, y) {
vec[idx..idx + px_size as usize].copy_from_slice(&px.0);
}
}
}
vec
}
fn path_img_key(path: &Path) -> String {
let mut rev_iter = path.components().rev();
let fname = rev_iter.next().unwrap();
let folder = rev_iter.next().unwrap();
let fname: &Path = fname.as_ref();
let folder: &Path = folder.as_ref();
folder
.join(fname.file_stem().unwrap())
.display()
.to_string()
}
#[test]
fn test_path_img_key() {
assert_eq!(
&path_img_key("/home/person/res/graphics/tiles/foo.png".as_ref()),
"tiles/foo"
);
}
fn walk_graphics(mut f: impl FnMut(&Path)) {
for en in walkdir::WalkDir::new("res/graphics") {
let en = en.unwrap();
if en.file_type().is_file() {
f(en.path());
}
}
}

View file

@ -4,11 +4,11 @@ use std::ops::Index;
use egui_inspect::derive::Inspect;
use serde::{Deserialize, Serialize};
use sfml::graphics::IntRect;
use crate::{
graphics::{ScreenSc, ScreenVec},
math::TILE_SIZE,
math::{IntRect, TILE_SIZE},
texture_atlas::RectMap,
world::TileId,
};
@ -17,9 +17,11 @@ pub struct TileDef {
pub bb: Option<TileBb>,
/// Whether the tile emits light, and the light source offset
pub light: Option<ScreenVec>,
pub atlas_offset: AtlasOffset,
/// Platform behavior: Horizontally passable, vertically passable upwards
pub platform: bool,
#[serde(default)]
pub graphic_name: String,
pub tex_rect: IntRect,
}
const DEFAULT_TILE_BB: TileBb = TileBb {
@ -44,9 +46,18 @@ pub struct TileDb {
impl Default for TileDb {
fn default() -> Self {
let unknown = TileDef {
bb: None,
light: Some(ScreenVec {
x: TILE_SIZE as ScreenSc / 2,
y: TILE_SIZE as ScreenSc / 2,
}),
platform: false,
graphic_name: String::from("tiles/unknown"),
tex_rect: IntRect::default(),
};
Self {
// Add empty/air tile
db: vec![EMPTY],
db: vec![EMPTY, unknown],
}
}
}
@ -56,52 +67,26 @@ const EMPTY: TileDef = TileDef {
light: None,
// Rendering empty tile is actually special cased, and no rendering is done.
// But just in case, put the offset to UNKNOWN
atlas_offset: UNKNOWN_ATLAS_OFF,
tex_rect: IntRect {
x: 0,
y: 0,
w: 0,
h: 0,
},
platform: false,
graphic_name: String::new(),
};
impl Index<TileId> for TileDb {
type Output = TileDef;
fn index(&self, index: TileId) -> &Self::Output {
self.db.get(index as usize).unwrap_or(&UNKNOWN_TILE)
self.db.get(index as usize).unwrap_or_else(|| {
&self.db[1] // Unknown tile def is stored at index 1
})
}
}
#[derive(Debug, Inspect, Serialize, Deserialize)]
pub struct AtlasOffset {
pub x: u16,
pub y: u16,
}
impl AtlasOffset {
pub(crate) fn to_sf_rect(&self) -> IntRect {
IntRect {
left: self.x as i32,
top: self.y as i32,
width: TILE_SIZE as i32,
height: TILE_SIZE as i32,
}
}
}
impl Default for AtlasOffset {
fn default() -> Self {
UNKNOWN_ATLAS_OFF
}
}
const UNKNOWN_ATLAS_OFF: AtlasOffset = AtlasOffset { x: 320, y: 0 };
static UNKNOWN_TILE: TileDef = TileDef {
bb: None,
light: Some(ScreenVec {
x: TILE_SIZE as ScreenSc / 2,
y: TILE_SIZE as ScreenSc / 2,
}),
atlas_offset: UNKNOWN_ATLAS_OFF,
platform: false,
};
const PATH: &str = "tiles.dat";
impl TileDb {
@ -129,4 +114,12 @@ impl TileDb {
Err(e) => log::warn!("Failed to save tile db: {e}"),
}
}
pub(crate) fn update_rects(&mut self, rects: &RectMap) {
for def in &mut self.db {
if !def.graphic_name.is_empty() {
def.tex_rect = rects[&def.graphic_name];
}
}
}
}

View file

@ -78,7 +78,7 @@ impl World {
chk.at_mut(local)
}
pub fn save(&self) {
let result = std::fs::create_dir(&self.path);
let result = std::fs::create_dir_all(&self.path);
log::info!("{result:?}");
self.save_meta();
self.player.save(&self.path);

View file

@ -34,7 +34,7 @@ impl Worldgen {
.add(
Tile::new(Tl {
bg: 9,
mid: 2,
mid: 3,
fg: 6,
})
.when(constraint!(nm.clone(), < -0.8)),
@ -43,7 +43,7 @@ impl Worldgen {
.add(
Tile::new(Tl {
bg: 9,
mid: 2,
mid: 3,
fg: 0,
})
.when(constraint!(nm.clone(), < -0.1)),
@ -52,7 +52,7 @@ impl Worldgen {
.add(
Tile::new(Tl {
bg: 7,
mid: 1,
mid: 2,
fg: 0,
})
.when(constraint!(nm, < 0.45)),