Imp in 64x64 when close, 32x32 at normal zoom; detailed sprites in their own texture

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 21:17:17 +01:00
1 parent 2d1ed177a7
commit 69a6b13006
9 files changed
+173 -89

No files matched your search

+15
View File
@@ -108,3 +108,18 @@ The game copies `imp-32.png` and `imp-hack-32.png` to `assets/sprites/`.
The gold sack is not generated: it is a small hand-made pixel pattern (`SACK_HD`, `SACK_SMALL` The gold sack is not generated: it is a small hand-made pixel pattern (`SACK_HD`, `SACK_SMALL`
in `src/gfx.rs`) that the game places on every frame when the atlas is built. in `src/gfx.rs`) that the game places on every frame when the atlas is built.
## 64x64 (2026-11-07)
The high-res style shows the imp in 64x64 when close and in 32x32 at the normal zoom (both from
the same raw sheets; a 64x64 sprite shown smaller would lose pixels and flicker). The 64x64
sheets use the body scale 60 pixels, the feet row 63, 24 colours plus the iron greys:
```bash
python3 $T raw-1.png imp --views 5 --size 64 --colors 24 --crop 0,0.27,1,0.72 --step 9.217 --feet 63 \
--add-colors "150,152,168;104,106,122"
# hack rows: --size 64 --step <body height>/60 --feet 63 --palette imp-64.png --outline-weight 0.6
```
Results: `imp-64.png`, `imp-hack-64.png`, preview `imp-hack-64-preview.png` (idle views in the
right column). The game copies them to `assets/sprites/`.
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+142 -79
View File
@@ -98,6 +98,7 @@ pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glo
pub struct Gfx { pub struct Gfx {
pub atlas: Texture2D, pub atlas: Texture2D,
pub sprites: Texture2D, pub sprites: Texture2D,
pub hd_sprites: Texture2D, // detailed sprites (see HD_SPRITES, hd_cell_view)
pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*) pub cursors: Texture2D, // mouse pointer frames, 16x16 each (see CURSOR_*)
pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs) pub badges: Texture2D, // achievement badges, 32x32 each (see badges.rs)
pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE pub font: Texture2D, // Dinowyde (glyphs of 6x8 px ordered by Unicode, see font_slot) and DINOBYTE
@@ -1368,36 +1369,46 @@ fn gen_atlas() -> Image {
const SPR: usize = 10; const SPR: usize = 10;
const SPR_CELL: u32 = 12; const SPR_CELL: u32 = 12;
// the atlas holds every sprite pixel as SPR_K x SPR_K texels, so detailed sprites (HD_SPRITES) // the atlas holds every sprite pixel as SPR_K x SPR_K texels: a cell is SPR_CELL * SPR_K texels
// fit into the same cells: a cell is SPR_CELL * SPR_K = 36 texels
const SPR_K: u32 = 3; const SPR_K: u32 = 3;
pub const ACELL: u32 = SPR_CELL * SPR_K; pub const ACELL: u32 = SPR_CELL * SPR_K;
// Detailed 32x32 sprites (generated, see art/generated/), used instead of the 10x10 ones in the // Detailed sprites (generated, see art/generated/) in their own texture, used instead of the
// high-res graphics style when close enough (render3d::billboards). Transparent background, the // 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of detail from
// feet on the bottom row. `idle`: the five views side by side (VIEWS); `hack`: the digging // the same pictures: 64x64 when the sprite gets enough screen pixels (close), 32x32 otherwise,
// animation, HACK_FRAMES frames side by side, one row per view. No gear yet. // so neither loses pixels nor flickers. Sheets: transparent, the feet on the bottom row;
// `idle*`: the five views side by side (VIEWS); `hack*`: the digging animation, HACK_FRAMES
// frames side by side, one row per view. No gear yet.
struct HdSprite { struct HdSprite {
kind: EK, kind: EK,
idle: &'static [u8], idle: &'static [u8],
hack: &'static [u8], hack: &'static [u8],
idle_32: &'static [u8],
hack_32: &'static [u8],
} }
const HD_SPRITES: [HdSprite; 1] = [HdSprite { const HD_SPRITES: [HdSprite; 1] = [HdSprite {
kind: EK::Worker, kind: EK::Worker,
idle: include_bytes!("../assets/sprites/imp-32.png"), idle: include_bytes!("../assets/sprites/imp-64.png"),
hack: include_bytes!("../assets/sprites/imp-hack-32.png"), hack: include_bytes!("../assets/sprites/imp-hack-64.png"),
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
hack_32: include_bytes!("../assets/sprites/imp-hack-32.png"),
}]; }];
const HD: u32 = 32;
pub const HACK_FRAMES: u32 = 4; pub const HACK_FRAMES: u32 = 4;
// atlas columns of a detailed sprite: idle and the hack frames, each without and with the sack // a detailed sprite's cell: the same shape as a 10x10 sprite's 12x12 cell (6 texels per pixel),
// so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would
pub const HD_CELL: u32 = SPR_CELL * 6;
// columns per detailed sprite and detail level: idle and the hack frames, each without and with
// the sack; then the same for the 32x32 level
const HD_COLS: u32 = (1 + HACK_FRAMES) * 2; const HD_COLS: u32 = (1 + HACK_FRAMES) * 2;
pub const HD_TEX_W: f32 = (HD_SPRITES.len() as u32 * HD_COLS * 2 * HD_CELL) as f32;
pub const HD_TEX_H: f32 = (VIEWS * 2 * HD_CELL) as f32;
// kinds that carry treasure in a sack on the back (instead of the coin sign over the head); // kinds that carry treasure in a sack on the back (instead of the coin sign over the head);
// their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS // their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS
const SACK_KINDS: [EK; 1] = [EK::Worker]; const SACK_KINDS: [EK; 1] = [EK::Worker];
// the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening) // the sack (k outline, B burlap, b its shade and the rope, y/Y gold coins at the opening)
const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."]; const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."];
const SACK_HD: [&str; 12] = [ const SACK_32: [&str; 12] = [
"....kkkk....", "....kkkk....",
"...kyYYyk...", "...kyYYyk...",
"..kyYyyYyk..", "..kyYyyYyk..",
@@ -1411,6 +1422,31 @@ const SACK_HD: [&str; 12] = [
"..kBBBBBBk..", "..kBBBBBBk..",
"...kkkkkk...", "...kkkkkk...",
]; ];
// the same, finer for the 64x64 sprites (d: the shadow side)
const SACK_64: [&str; 22] = [
".......kkkkkk.......",
".....kkyYYYyykk.....",
"....kyYYyYYyyYyk....",
"...kyYyyYyYYyyYyk...",
"...kkyYYyyyYYyykk...",
"....kkbbbbbbbbkk....",
".....kbBbbbbBbk.....",
"......kBBBBBBk......",
".....kBBBBBBBBk.....",
"....kBBBBBBBBBBk....",
"...kBBBBbBBBBBBBk...",
"..kBBBBBBBBBbBBBBk..",
"..kBBbBBBBBBBBBBBk..",
".kBBBBBBBBBBBBbBBBk.",
".kBBBBBBBbBBBBBBBBk.",
".kBBBBBBBBBBBBBBBdk.",
".kBBbBBBBBBBBBBBddk.",
".kBBBBBBBBBBBBBdddk.",
"..kBBBBBBBBBBBdddk..",
"..kdBBBBBBBBBddddk..",
"...kkddddddddddkk...",
".....kkkkkkkkkk.....",
];
fn sprite_rows(k: EK) -> [&'static str; 10] { fn sprite_rows(k: EK) -> [&'static str; 10] {
match k { match k {
@@ -1868,7 +1904,7 @@ pub const SPRITE_KINDS: [EK; 31] = [
EK::Inquisitor, EK::Inquisitor,
]; ];
// width of the sprite texture in pixels // width of the sprite texture in pixels
pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL + HD_SPRITES.len() as u32 * HD_COLS * ACELL) as f32; pub const SPRITE_TEX_W: f32 = ((SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 * ACELL) as f32;
pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32; pub const SPRITE_TEX_H: f32 = (2 * VIEWS * GEAR_TIERS * ACELL) as f32;
pub fn sprite_index(k: EK) -> u32 { pub fn sprite_index(k: EK) -> u32 {
@@ -2105,6 +2141,7 @@ fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) {
'b' => c(108, 70, 40), 'b' => c(108, 70, 40),
'y' => c(232, 176, 48), 'y' => c(232, 176, 48),
'Y' => c(255, 232, 120), 'Y' => c(255, 232, 120),
'd' => c(112, 74, 42),
_ => continue, _ => continue,
}; };
let (x, y) = (x0 + px as i32, y0 + py as i32); let (x, y) = (x0 + px as i32, y0 + py as i32);
@@ -2119,30 +2156,32 @@ fn add_sack(g: &mut Cell, view: u32, pat: &[&str], front_dx: (f32, f32)) {
} }
} }
// writes a cell into the atlas: column, cell row (the white silhouette goes into the row below), // writes a cell into an atlas of `cell` x `cell` texel cells: column, cell row (the white
// every cell pixel as k x k texels // silhouette goes into the row below), every cell pixel as k x k texels
fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32) { fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32, cell: u32) {
for (y, line) in g.iter().enumerate() { for (y, line) in g.iter().enumerate() {
for (x, p) in line.iter().enumerate() { for (x, p) in line.iter().enumerate() {
let Some(c) = p else { continue }; let Some(c) = p else { continue };
for d in 0..k * k { for d in 0..k * k {
let (px, py) = (col * ACELL + x as u32 * k + d % k, row * ACELL + y as u32 * k + d / k); let (px, py) = (col * cell + x as u32 * k + d % k, row * cell + y as u32 * k + d / k);
img.set_pixel(px, py, *c); img.set_pixel(px, py, *c);
img.set_pixel(px, py + ACELL, WHITE); img.set_pixel(px, py + cell, WHITE);
} }
} }
} }
} }
// a 32x32 picture from a sheet into a 36x36 cell (the feet on the 10x10 sprite's bottom row) // an n x n picture from a sheet into a cell grid of `size` (centred, the feet on the bottom row
fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell { // of a 10x10 sprite: 11/12 of the cell)
let mut g = vec![vec![None; ACELL as usize]; ACELL as usize]; fn hd_cell(src: &Image, sx: u32, sy: u32, n: u32, size: u32) -> Cell {
for y in 0..HD { let mut g = vec![vec![None; size as usize]; size as usize];
for x in 0..HD { let (ox, oy) = ((size - n) / 2, size * 11 / 12 - n);
for y in 0..n {
for x in 0..n {
if sx + x < src.width as u32 && sy + y < src.height as u32 { if sx + x < src.width as u32 && sy + y < src.height as u32 {
let p = src.get_pixel(sx + x, sy + y); let p = src.get_pixel(sx + x, sy + y);
if p.a > 0.5 { if p.a > 0.5 {
g[1 + y as usize][2 + x as usize] = Some(Color::new(p.r, p.g, p.b, 1.0)); g[(oy + y) as usize][(ox + x) as usize] = Some(Color::new(p.r, p.g, p.b, 1.0));
} }
} }
} }
@@ -2150,9 +2189,9 @@ fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell {
g g
} }
// The sprite atlas. Columns: SPRITE_KINDS, SACK_KINDS (with the sack), then HD_COLS per // The sprite atlas of the 10x10 sprites. Columns: SPRITE_KINDS, then SACK_KINDS with the sack.
// detailed sprite. 10x10 sprites: row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, // Row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, the row after it the white
// the row after it the white silhouette (hit flash). Detailed sprites: row view * 2 (+1 white). // silhouette (hit flash).
fn gen_sprites() -> Image { fn gen_sprites() -> Image {
let n = SPRITE_KINDS.len() as u32; let n = SPRITE_KINDS.len() as u32;
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 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));
@@ -2161,27 +2200,40 @@ fn gen_sprites() -> Image {
for tier in 0..GEAR_TIERS { for tier in 0..GEAR_TIERS {
let row = (view * GEAR_TIERS + tier) * 2; let row = (view * GEAR_TIERS + tier) * 2;
for (i, &k) in SPRITE_KINDS.iter().enumerate() { for (i, &k) in SPRITE_KINDS.iter().enumerate() {
put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K); put_cell(&mut img, i as u32, row, &small(k, tier, view), SPR_K, ACELL);
} }
for (i, &k) in SACK_KINDS.iter().enumerate() { for (i, &k) in SACK_KINDS.iter().enumerate() {
let mut g = small(k, tier, view); let mut g = small(k, tier, view);
add_sack(&mut g, view, &SACK_SMALL, (-0.5, -0.5)); add_sack(&mut g, view, &SACK_SMALL, (-0.5, -0.5));
put_cell(&mut img, n + i as u32, row, &g, SPR_K); put_cell(&mut img, n + i as u32, row, &g, SPR_K, ACELL);
} }
} }
} }
let base = n + SACK_KINDS.len() as u32; img
}
// The texture of the detailed sprites (HD_SPRITES): per sprite HD_COLS columns of 64x64
// pictures, then HD_COLS of 32x32 ones (two texels per pixel); row view * 2, the row after it
// the white silhouette.
fn gen_hd_sprites() -> Image {
let mut img = Image::gen_image_color(HD_TEX_W as u16, HD_TEX_H as u16, Color::new(0.0, 0.0, 0.0, 0.0));
let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite");
for (j, hd) in HD_SPRITES.iter().enumerate() { for (j, hd) in HD_SPRITES.iter().enumerate() {
let idle = Image::from_file_with_format(hd.idle, Some(ImageFormat::Png)).expect("hd sprite"); // (idle, hack, picture size, cell grid, texels per pixel, sack, where the sack shows from the front)
let hack = Image::from_file_with_format(hd.hack, Some(ImageFormat::Png)).expect("hd sprite"); let levels = [
for view in 0..VIEWS { (png(hd.idle), png(hd.hack), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)),
for f in 0..1 + HACK_FRAMES { (png(hd.idle_32), png(hd.hack_32), 32, HD_CELL / 2, 2, &SACK_32[..], (0.7, -0.65)),
let g = if f == 0 { hd_cell(&idle, view * HD, 0) } else { hd_cell(&hack, (f - 1) * HD, view * HD) }; ];
let col = base + j as u32 * HD_COLS + f * 2; for (l, (idle, hack, n, size, k, sack, dx)) in levels.iter().enumerate() {
put_cell(&mut img, col, view * 2, &g, 1); for view in 0..VIEWS {
let mut s = g; for f in 0..1 + HACK_FRAMES {
add_sack(&mut s, view, &SACK_HD, (0.7, -0.65)); let g = if f == 0 { hd_cell(idle, view * n, 0, *n, *size) } else { hd_cell(hack, (f - 1) * n, view * n, *n, *size) };
put_cell(&mut img, col + 1, view * 2, &s, 1); let col = (j as u32 * 2 + l as u32) * HD_COLS + f * 2;
put_cell(&mut img, col, view * 2, &g, *k, HD_CELL);
let mut s = g;
add_sack(&mut s, view, sack, *dx);
put_cell(&mut img, col + 1, view * 2, &s, *k, HD_CELL);
}
} }
} }
} }
@@ -2308,7 +2360,9 @@ impl Gfx {
cursors.set_filter(FilterMode::Nearest); cursors.set_filter(FilterMode::Nearest);
let badges = Texture2D::from_image(&crate::badges::gen_badges()); let badges = Texture2D::from_image(&crate::badges::gen_badges());
badges.set_filter(FilterMode::Nearest); badges.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, cursors, badges, font, font_map } let hd_sprites = Texture2D::from_image(&gen_hd_sprites());
hd_sprites.set_filter(FilterMode::Nearest);
Gfx { atlas, sprites, hd_sprites, cursors, badges, font, font_map }
} }
pub fn cell(id: u32) -> Rect { pub fn cell(id: u32) -> Rect {
@@ -2335,14 +2389,15 @@ impl Gfx {
} }
// the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view) // the cell of a detailed sprite's view, if the kind has one (same layout as sprite_cell_view)
// the cell of a detailed sprite, if the kind has one: a view, standing (`hack` None) or a // the cell of a detailed sprite in `hd_sprites`, if the kind has one: a view, standing
// frame of the digging animation, with or without the sack // (`hack` None) or a frame of the digging animation, with or without the sack; `fine`: the
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool) -> Option<Rect> { // 64x64 level, else the 32x32 one
pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option<u32>, sack: bool, fine: bool) -> Option<Rect> {
let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32; let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32;
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES); let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
let col = (SPRITE_KINDS.len() + SACK_KINDS.len()) as u32 + j * HD_COLS + f * 2 + sack as u32; let col = (j * 2 + !fine as u32) * HD_COLS + f * 2 + sack as u32;
let row = view.min(VIEWS - 1) * 2 + white as u32; let row = view.min(VIEWS - 1) * 2 + white as u32;
Some(Rect::new((col * ACELL) as f32, (row * ACELL) as f32, ACELL as f32, ACELL as f32)) Some(Rect::new((col * HD_CELL) as f32, (row * HD_CELL) as f32, HD_CELL as f32, HD_CELL as f32))
} }
// whether a kind carries its treasure in a sack on the back // whether a kind carries its treasure in a sack on the back
@@ -2582,36 +2637,46 @@ mod sprite_tests {
use super::*; use super::*;
fn opaque(img: &Image, r: Rect) -> usize { fn opaque(img: &Image, r: Rect) -> usize {
(0..ACELL * ACELL).filter(|i| img.get_pixel(r.x as u32 + i % ACELL, r.y as u32 + i / ACELL).a > 0.5).count() let n = r.w as u32;
(0..n * n).filter(|i| img.get_pixel(r.x as u32 + i % n, r.y as u32 + i / n).a > 0.5).count()
} }
// texels that differ between two cells // texels that differ between two cells
fn differ(img: &Image, a: Rect, b: Rect) -> usize { fn differ(img: &Image, a: Rect, b: Rect) -> usize {
(0..ACELL * ACELL) let n = a.w as u32;
(0..n * n)
.filter(|i| { .filter(|i| {
let p = img.get_pixel(a.x as u32 + i % ACELL, a.y as u32 + i / ACELL); let p = img.get_pixel(a.x as u32 + i % n, a.y as u32 + i / n);
let q = img.get_pixel(b.x as u32 + i % ACELL, b.y as u32 + i / ACELL); let q = img.get_pixel(b.x as u32 + i % n, b.y as u32 + i / n);
(p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a) (p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a)
}) })
.count() .count()
} }
#[test] #[test]
fn detailed_sprites_in_the_atlas() { fn detailed_sprites() {
let img = gen_sprites(); let img = gen_hd_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H)); assert_eq!((img.width as f32, img.height as f32), (HD_TEX_W, HD_TEX_H));
for view in 0..VIEWS { for fine in [true, false] {
for hack in [None, Some(0), Some(1), Some(2), Some(3)] { for view in 0..VIEWS {
let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false).expect("the imp has a detailed sprite"); for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false).unwrap(); let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false, fine).expect("the imp has a detailed sprite");
let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true).unwrap(); let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false, fine).unwrap();
assert!(opaque(&img, r) > 150, "view {view} {hack:?} drawn"); let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true, fine).unwrap();
assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches"); assert!(opaque(&img, r) > 600, "view {view} {hack:?} {fine} drawn");
assert!(differ(&img, s, r) > 15, "view {view} {hack:?}: the sack shows"); assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches");
assert!(differ(&img, s, r) > 60, "view {view} {hack:?} {fine}: the sack shows");
}
} }
} }
assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false).is_none()); assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false, true).is_none());
// the 10x10 sprites: every pixel a 3x3 block; the imp's sack variant }
#[test]
fn small_sprites_and_sack() {
let img = gen_sprites();
assert_eq!((img.width as f32, img.height as f32), (SPRITE_TEX_W, SPRITE_TEX_H));
// every pixel a 3x3 block
let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0); let r = Gfx::sprite_cell_view(EK::Knight, false, 1, 0);
for y in 0..SPR_CELL { for y in 0..SPR_CELL {
for x in 0..SPR_CELL { for x in 0..SPR_CELL {
@@ -2628,32 +2693,30 @@ mod sprite_tests {
} }
} }
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's cells to /tmp for a look: // `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed cells to /tmp for
// rows: 10x10, 10x10 with sack, then idle and the hack frames without and with sack // a look: rows idle and the hack frames, without and with sack; 64x64 left, 32x32 right
#[test] #[test]
#[ignore] #[ignore]
fn dump_imp_cells() { fn dump_imp_cells() {
let img = gen_sprites(); let img = gen_hd_sprites();
let mut cells: Vec<Vec<Rect>> = Vec::new(); let mut rows: Vec<Vec<Rect>> = Vec::new();
for sack in [false, true] {
let r: Vec<Rect> = (0..VIEWS).map(|v| if sack { Gfx::sprite_cell_sack(EK::Worker, false, 1, v).unwrap() } else { Gfx::sprite_cell_view(EK::Worker, false, 1, v) }).collect();
cells.push(r);
}
for sack in [false, true] { for sack in [false, true] {
for hack in [None, Some(0), Some(1), Some(2), Some(3)] { for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
cells.push((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack).unwrap()).collect()); let mut r: Vec<Rect> = (0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, true).unwrap()).collect();
r.extend((0..VIEWS).map(|v| Gfx::hd_cell_view(EK::Worker, false, v, hack, sack, false).unwrap()));
rows.push(r);
} }
} }
let z = 3; let c = HD_CELL;
let (w, h) = (ACELL * z * VIEWS, ACELL * z * cells.len() as u32); let (w, h) = (c * VIEWS * 2, c * rows.len() as u32);
let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0)); let mut out = Image::gen_image_color(w as u16, h as u16, Color::new(0.35, 0.33, 0.38, 1.0));
for (row, list) in cells.iter().enumerate() { for (row, list) in rows.iter().enumerate() {
for (view, r) in list.iter().enumerate() { for (i, r) in list.iter().enumerate() {
for y in 0..ACELL * z { for y in 0..c {
for x in 0..ACELL * z { for x in 0..c {
let p = img.get_pixel(r.x as u32 + x / z, r.y as u32 + y / z); let p = img.get_pixel(r.x as u32 + x, r.y as u32 + y);
if p.a > 0.5 { if p.a > 0.5 {
out.set_pixel(view as u32 * ACELL * z + x, h - 1 - (row as u32 * ACELL * z + y), p); out.set_pixel(i as u32 * c + x, h - 1 - (row as u32 * c + y), p);
} }
} }
} }
+16 -10
View File
@@ -1563,8 +1563,9 @@ impl R3d {
// sprites as billboards // sprites as billboards
// the world's pixel rows (detailed sprites only where they get enough pixels) // the world's pixel rows (detailed sprites only where they get enough pixels)
let world_rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32); let world_rows = pix.map_or(screen_height() * screen_dpi_scale(), |(_, th, _)| th as f32);
if let Some(mesh) = self.billboards(g, gfx, cam, t, world_rows) { let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows);
draw_mesh(&mesh); for mesh in [small, detailed].iter().flatten() {
draw_mesh(mesh);
} }
// solid unlit objects (default material, depth tested) // solid unlit objects (default material, depth tested)
gl_use_default_material(); gl_use_default_material();
@@ -1638,7 +1639,8 @@ impl R3d {
self.draw_2d(g, gfx, cam, &m, t); self.draw_2d(g, gfx, cam, &m, t);
} }
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Option<Mesh> { // the units as billboards: the 10x10 sprites and the detailed ones (two textures)
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> (Option<Mesh>, Option<Mesh>) {
let (_, r, _) = cam.basis(); let (_, r, _) = cam.basis();
let toward = cam.eye_dir(); let toward = cam.eye_dir();
let amb = |x: f32, z: f32| { let amb = |x: f32, z: f32| {
@@ -1656,6 +1658,7 @@ impl R3d {
let rg = vec2(r.x, r.z).normalize_or_zero(); let rg = vec2(r.x, r.z).normalize_or_zero();
let tg = vec2(toward.x, toward.z).normalize_or_zero(); let tg = vec2(toward.x, toward.z).normalize_or_zero();
let mut mb = MB::new(); let mut mb = MB::new();
let mut mb_hd = MB::new();
let sw = SPRITE_TEX_W; let sw = SPRITE_TEX_W;
let sh = SPRITE_TEX_H; let sh = SPRITE_TEX_H;
// detailed sprites only in the high-res style, and only where a sprite's cell (1.2 sizes // detailed sprites only in the high-res style, and only where a sprite's cell (1.2 sizes
@@ -1761,19 +1764,24 @@ impl R3d {
} }
let white = e.flash > 0.07; let white = e.flash > 0.07;
let v = if e.captive != 0 { 0 } else { view }; let v = if e.captive != 0 { 0 } else { view };
let detailed = hd && w * 1.2 * px_per_tile >= crate::gfx::ACELL as f32 * 0.85; // screen pixels of the cell per texel of a detailed cell (64x64 level), the 32x32
// level has two texels per pixel
let texel = w * 1.2 * px_per_tile / crate::gfx::HD_CELL as f32;
let detailed = hd && texel * 2.0 >= 0.85;
let fine = texel >= 0.85;
// treasure in a sack on the back; digging as a drawn animation (detailed sprites), // treasure in a sack on the back; digging as a drawn animation (detailed sprites),
// frame by frame with the same rhythm as the swing below // frame by frame with the same rhythm as the swing below
let sack = e.carry > 0 && Gfx::has_sack(e.kind); let sack = e.carry > 0 && Gfx::has_sack(e.kind);
let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_)); let hacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Dig(_) | Job::TearBridge(_));
let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32; let frame = ((ph * 1.3 / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::HACK_FRAMES as f32) as u32;
let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack) } else { None }; let hd = if detailed { Gfx::hd_cell_view(e.kind, white, v, hacking.then_some(frame), sack, fine) } else { None };
if hd.is_some() && hacking { if hd.is_some() && hacking {
// the frames swing the pickaxe: no tilting of the whole figure // the frames swing the pickaxe: no tilting of the whole figure
rot = 0.0; rot = 0.0;
lunge = 0.0; lunge = 0.0;
} }
let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v)); let src = hd.or_else(|| sack.then(|| Gfx::sprite_cell_sack(e.kind, white, e.level, v)).flatten()).unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
let (sw, sh) = if hd.is_some() { (crate::gfx::HD_TEX_W, crate::gfx::HD_TEX_H) } else { (sw, sh) };
let (mut u0, v0) = (src.x / sw, src.y / sh); let (mut u0, v0) = (src.x / sw, src.y / sh);
let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh); let (mut u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh);
if mirror { if mirror {
@@ -1813,6 +1821,7 @@ impl R3d {
}; };
// the 12x12 cell (sprite with a one pixel border for the gear): the sprite's feet // the 12x12 cell (sprite with a one pixel border for the gear): the sprite's feet
// (bottom of its 11th pixel row) stay on the ground // (bottom of its 11th pixel row) stay on the ground
let mb = if hd.is_some() { &mut mb_hd } else { &mut mb };
mb.quad( mb.quad(
[corner(-w * 0.6, h * 1.1), corner(w * 0.6, h * 1.1), corner(w * 0.6, -h * 0.1), corner(-w * 0.6, -h * 0.1)], [corner(-w * 0.6, h * 1.1), corner(w * 0.6, h * 1.1), corner(w * 0.6, -h * 0.1), corner(-w * 0.6, -h * 0.1)],
[vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)], [vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)],
@@ -1820,11 +1829,8 @@ impl R3d {
[toward; 4], [toward; 4],
); );
} }
if mb.v.is_empty() { let mesh = |mb: MB, tex: &Texture2D| if mb.v.is_empty() { None } else { Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(tex.clone()) }) };
None (mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites))
} else {
Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(gfx.sprites.clone()) })
}
} }
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) { fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {