Detailed 32 px imp sprite and finer world pixel rows (540, 1080)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 20:41:29 +01:00
1 parent 10af85fd95
commit 928c34e78b
7 files changed
+121 -18

No files matched your search

+99 -10
View File
@@ -1368,6 +1368,16 @@ fn gen_atlas() -> Image {
const SPR: usize = 10;
const SPR_CELL: u32 = 12;
// the atlas holds every sprite pixel as SPR_K x SPR_K texels, so detailed sprites (HD_SPRITES)
// fit into the same cells: a cell is SPR_CELL * SPR_K = 36 texels
const SPR_K: u32 = 3;
pub const ACELL: u32 = SPR_CELL * SPR_K;
// Detailed 32x32 sprites (generated, see art/generated/), used instead of the 10x10 ones when
// the world is rendered fine enough (render3d::billboards). Five views side by side like VIEWS,
// transparent background, the feet on the bottom row. No gear yet: every tier looks the same.
const HD_SPRITES: [(EK, &[u8]); 1] = [(EK::Worker, include_bytes!("../assets/sprites/imp-32.png"))];
const HD: u32 = 32;
fn sprite_rows(k: EK) -> [&'static str; 10] {
match k {
@@ -1825,8 +1835,8 @@ pub const SPRITE_KINDS: [EK; 31] = [
EK::Inquisitor,
];
// width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = (SPRITE_KINDS.len() * 12) as f32;
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * SPR_CELL) as f32;
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + HD_SPRITES.len()) as u32 * ACELL) as f32;
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32;
pub fn sprite_index(k: EK) -> u32 {
SPRITE_KINDS.iter().position(|&s| s == k).unwrap_or(0) as u32
@@ -2022,10 +2032,14 @@ fn sprite_with_gear(k: EK, tier: u32, view: u32) -> [[Option<Color>; 12]; 12] {
}
// row (view * GEAR_TIERS + tier) * 2: sprites with the gear of the tier, the row after it:
// white silhouettes (hit flash)
// white silhouettes (hit flash). Columns: SPRITE_KINDS, then HD_SPRITES.
fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32;
let mut img = Image::gen_image_color((n * SPR_CELL) as u16, (2 * VIEWS * GEAR_TIERS * SPR_CELL) as u16, Color::new(0.0, 0.0, 0.0, 0.0));
let mut img = Image::gen_image_color(SPRITE_TEX_W as u16, SPRITE_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
let put = |img: &mut Image, px: u32, py: u32, col: Color| {
img.set_pixel(px, py, col);
img.set_pixel(px, py + ACELL, WHITE);
};
for (i, &k) in SPRITE_KINDS.iter().enumerate() {
for view in 0..VIEWS {
for tier in 0..GEAR_TIERS {
@@ -2033,9 +2047,27 @@ fn gen_sprites() -> Image {
for (y, row) in g.iter().enumerate() {
for (x, px) in row.iter().enumerate() {
if let Some(col) = px {
let (px, py) = (i as u32 * SPR_CELL + x as u32, (view * GEAR_TIERS + tier) * 2 * SPR_CELL + y as u32);
img.set_pixel(px, py, *col);
img.set_pixel(px, py + SPR_CELL, WHITE);
let (px, py) = (i as u32 * ACELL + x as u32 * SPR_K, (view * GEAR_TIERS + tier) * 2 * ACELL + y as u32 * SPR_K);
for d in 0..SPR_K * SPR_K {
put(&mut img, px + d % SPR_K, py + d / SPR_K, *col);
}
}
}
}
}
}
}
for (j, (_, png)) in HD_SPRITES.iter().enumerate() {
let src = Image::from_file_with_format(png, Some(ImageFormat::Png)).expect("hd sprite");
for view in 0..VIEWS {
for tier in 0..GEAR_TIERS {
for y in 0..HD.min(src.height as u32) {
for x in 0..HD {
let col = src.get_pixel(view * HD + x, y);
if col.a > 0.5 {
// the feet on the 10x10 sprite's bottom row (cell texel 33)
let (px, py) = ((n + j as u32) * ACELL + 2 + x, (view * GEAR_TIERS + tier) * 2 * ACELL + 1 + y);
put(&mut img, px, py, Color::new(col.r, col.g, col.b, 1.0));
}
}
}
@@ -2175,7 +2207,8 @@ impl Gfx {
// the plain 10x10 sprite (interface icons)
pub fn sprite_rect(k: EK, white: bool) -> Rect {
let i = sprite_index(k);
Rect::new((i * SPR_CELL + 1) as f32, (1 + if white { SPR_CELL } else { 0 }) as f32, SPR as f32, SPR as f32)
let k = SPR_K as f32;
Rect::new((i * ACELL) as f32 + k, (if white { ACELL } else { 0 }) as f32 + k, SPR as f32 * k, SPR as f32 * k)
}
// the whole 12x12 cell with the gear of a level (billboards)
@@ -2187,7 +2220,14 @@ impl Gfx {
pub fn sprite_cell_view(k: EK, white: bool, level: u32, view: u32) -> Rect {
let i = sprite_index(k);
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
Rect::new((i * SPR_CELL) as f32, (row * SPR_CELL) as f32, SPR_CELL as f32, SPR_CELL as f32)
Rect::new((i * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32)
}
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
pub fn hd_cell_view(k: EK, white: bool, level: u32, view: u32) -> Option<Rect> {
let j = HD_SPRITES.iter().position(|(h, _)| *h == k)? as u32;
let row = (view.min(VIEWS - 1) * GEAR_TIERS + gear_tier(level)) * 2 + white as u32;
Some(Rect::new(((SPRITE_KINDS.len() as u32 + j) * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32))
}
pub fn tile_tex(kind: Kind, owner: Owner, var: u8, reinforced: bool) -> u32 {
@@ -2410,6 +2450,55 @@ impl Gfx {
}
}
#[cfg(test)]
mod sprite_tests {
use super::*;
#[test]
fn detailed_sprites_in_the_atlas() {
let img = gen_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
for view in 0..VIEWS {
let r = Gfx::hd_cell_view(EK::Worker, false, 1, view).expect("the imp has a detailed sprite");
let w = Gfx::hd_cell_view(EK::Worker, true, 1, view).unwrap();
let opaque = |r: Rect| (0..ACELL * ACELL).filter(|i| img.get_pixel(r.x as u32 + i % ACELL, r.y as u32 + i / ACELL).a > 0.5).count();
assert!(opaque(r) > 150, "view {view} drawn");
assert_eq!(opaque(r), opaque(w), "white silhouette matches");
}
assert!(Gfx::hd_cell_view(EK::Knight, false, 1, 0).is_none());
// the 10x10 sprites: every pixel a 3x3 block
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
for y in 0..SPR_CELL {
for x in 0..SPR_CELL {
let p = img.get_pixel(r.x as u32 + x * SPR_K, r.y as u32 + y * SPR_K);
let q = img.get_pixel(r.x as u32 + x * SPR_K + 2, r.y as u32 + y * SPR_K + 2);
assert_eq!((p.r, p.g, p.b, p.a), (q.r, q.g, q.b, q.a));
}
}
}
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's cells to /tmp for a look
#[test]
#[ignore]
fn dump_imp_cells() {
let img = gen_sprites();
let mut out = Image::gen_image_color((ACELL * VIEWS * 4) as u16, (ACELL * 8) as u16, Color::new(0.35, 0.33, 0.38, 1.0));
for view in 0..VIEWS {
for (row, r) in [Gfx::sprite_cell_view(EK::Worker, false, 1, view), Gfx::hd_cell_view(EK::Worker, false, 1, view).unwrap()].iter().enumerate() {
for y in 0..ACELL * 4 {
for x in 0..ACELL * 4 {
let p = img.get_pixel(r.x as u32 + x / 4, r.y as u32 + y / 4);
if p.a > 0.5 {
out.set_pixel(view * ACELL * 4 + x, (ACELL * 8 - 1) - (row as u32 * ACELL * 4 + y), p);
}
}
}
}
}
out.export_png("/tmp/imp-cells.png");
}
}
#[cfg(test)]
mod doc_images {
use super::*;
@@ -2438,7 +2527,7 @@ mod doc_images {
for &k in SPRITE_KINDS.iter() {
let id = crate::game::st(k).name.trim_start_matches("unit.");
for (lv, tag) in [(1u32, "1"), (5, "5"), (10, "10")] {
save(&sprites, Gfx::sprite_cell(k, false, lv), 8, &format!("docs/img/units/{}_{}.png", id, tag));
save(&sprites, Gfx::sprite_cell(k, false, lv), 3, &format!("docs/img/units/{}_{}.png", id, tag));
}
}
let atlas = gen_atlas();