diff --git a/art/generated/imp-sprite/PROMPTS.md b/art/generated/imp-sprite/PROMPTS.md index 596eccf..80efe4f 100644 --- a/art/generated/imp-sprite/PROMPTS.md +++ b/art/generated/imp-sprite/PROMPTS.md @@ -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` 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 /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/`. diff --git a/art/generated/imp-sprite/imp-64-preview.png b/art/generated/imp-sprite/imp-64-preview.png new file mode 100644 index 0000000..6606170 Binary files /dev/null and b/art/generated/imp-sprite/imp-64-preview.png differ diff --git a/art/generated/imp-sprite/imp-64.png b/art/generated/imp-sprite/imp-64.png new file mode 100644 index 0000000..6fbb9b3 Binary files /dev/null and b/art/generated/imp-sprite/imp-64.png differ diff --git a/art/generated/imp-sprite/imp-hack-64-preview.png b/art/generated/imp-sprite/imp-hack-64-preview.png new file mode 100644 index 0000000..62727ad Binary files /dev/null and b/art/generated/imp-sprite/imp-hack-64-preview.png differ diff --git a/art/generated/imp-sprite/imp-hack-64.png b/art/generated/imp-sprite/imp-hack-64.png new file mode 100644 index 0000000..a8b65bd Binary files /dev/null and b/art/generated/imp-sprite/imp-hack-64.png differ diff --git a/assets/sprites/imp-64.png b/assets/sprites/imp-64.png new file mode 100644 index 0000000..6fbb9b3 Binary files /dev/null and b/assets/sprites/imp-64.png differ diff --git a/assets/sprites/imp-hack-64.png b/assets/sprites/imp-hack-64.png new file mode 100644 index 0000000..a8b65bd Binary files /dev/null and b/assets/sprites/imp-hack-64.png differ diff --git a/src/gfx.rs b/src/gfx.rs index 02b923d..97d9e9c 100644 --- a/src/gfx.rs +++ b/src/gfx.rs @@ -98,6 +98,7 @@ pub const T_MOSS: u32 = 216; // 4 variants: mushroom grove ground, moss with glo pub struct Gfx { pub atlas: 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 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 @@ -1368,36 +1369,46 @@ 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 +// the atlas holds every sprite pixel as SPR_K x SPR_K texels: a cell is SPR_CELL * SPR_K 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 in the -// high-res graphics style when close enough (render3d::billboards). Transparent background, 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. +// Detailed sprites (generated, see art/generated/) in their own texture, used instead of the +// 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of detail from +// the same pictures: 64x64 when the sprite gets enough screen pixels (close), 32x32 otherwise, +// 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 { kind: EK, idle: &'static [u8], hack: &'static [u8], + idle_32: &'static [u8], + hack_32: &'static [u8], } const HD_SPRITES: [HdSprite; 1] = [HdSprite { kind: EK::Worker, - idle: include_bytes!("../assets/sprites/imp-32.png"), - hack: include_bytes!("../assets/sprites/imp-hack-32.png"), + idle: include_bytes!("../assets/sprites/imp-64.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; -// 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; +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); // their 10x10 sprite with the sack gets an own atlas column after SPRITE_KINDS 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) const SACK_SMALL: [&str; 5] = [".kk.", "kyYk", "kBbk", "kBBk", ".kk."]; -const SACK_HD: [&str; 12] = [ +const SACK_32: [&str; 12] = [ "....kkkk....", "...kyYYyk...", "..kyYyyYyk..", @@ -1411,6 +1422,31 @@ const SACK_HD: [&str; 12] = [ "..kBBBBBBk..", "...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] { match k { @@ -1868,7 +1904,7 @@ pub const SPRITE_KINDS: [EK; 31] = [ EK::Inquisitor, ]; // 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 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), 'y' => c(232, 176, 48), 'Y' => c(255, 232, 120), + 'd' => c(112, 74, 42), _ => continue, }; 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), -// every cell pixel as k x k texels -fn put_cell(img: &mut Image, col: u32, row: u32, g: &Cell, k: u32) { +// writes a cell into an atlas of `cell` x `cell` texel cells: column, cell row (the white +// 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, cell: u32) { for (y, line) in g.iter().enumerate() { for (x, p) in line.iter().enumerate() { let Some(c) = p else { continue }; 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 + 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) -fn hd_cell(src: &Image, sx: u32, sy: u32) -> Cell { - let mut g = vec![vec![None; ACELL as usize]; ACELL as usize]; - for y in 0..HD { - for x in 0..HD { +// an n x n picture from a sheet into a cell grid of `size` (centred, the feet on the bottom row +// of a 10x10 sprite: 11/12 of the cell) +fn hd_cell(src: &Image, sx: u32, sy: u32, n: u32, size: u32) -> Cell { + let mut g = vec![vec![None; size as usize]; size as usize]; + 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 { let p = src.get_pixel(sx + x, sy + y); 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 } -// The sprite atlas. Columns: SPRITE_KINDS, SACK_KINDS (with the sack), then HD_COLS per -// detailed sprite. 10x10 sprites: row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, -// the row after it the white silhouette (hit flash). Detailed sprites: row view * 2 (+1 white). +// The sprite atlas of the 10x10 sprites. Columns: SPRITE_KINDS, then SACK_KINDS with the sack. +// Row (view * GEAR_TIERS + tier) * 2 with the gear of the tier, the row after it the white +// silhouette (hit flash). fn gen_sprites() -> Image { 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)); @@ -2161,27 +2200,40 @@ fn gen_sprites() -> Image { for tier in 0..GEAR_TIERS { let row = (view * GEAR_TIERS + tier) * 2; 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() { let mut g = small(k, tier, view); 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() { - let idle = Image::from_file_with_format(hd.idle, Some(ImageFormat::Png)).expect("hd sprite"); - let hack = Image::from_file_with_format(hd.hack, Some(ImageFormat::Png)).expect("hd sprite"); - for view in 0..VIEWS { - for f in 0..1 + HACK_FRAMES { - 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; - put_cell(&mut img, col, view * 2, &g, 1); - let mut s = g; - add_sack(&mut s, view, &SACK_HD, (0.7, -0.65)); - put_cell(&mut img, col + 1, view * 2, &s, 1); + // (idle, hack, picture size, cell grid, texels per pixel, sack, where the sack shows from the front) + let levels = [ + (png(hd.idle), png(hd.hack), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)), + (png(hd.idle_32), png(hd.hack_32), 32, HD_CELL / 2, 2, &SACK_32[..], (0.7, -0.65)), + ]; + for (l, (idle, hack, n, size, k, sack, dx)) in levels.iter().enumerate() { + for view in 0..VIEWS { + for f in 0..1 + HACK_FRAMES { + let g = if f == 0 { hd_cell(idle, view * n, 0, *n, *size) } else { hd_cell(hack, (f - 1) * n, view * n, *n, *size) }; + 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); let badges = Texture2D::from_image(&crate::badges::gen_badges()); 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 { @@ -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, if the kind has one: a view, standing (`hack` None) or a - // frame of the digging animation, with or without the sack - pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option, sack: bool) -> Option { + // the cell of a detailed sprite in `hd_sprites`, if the kind has one: a view, standing + // (`hack` None) or a frame of the digging animation, with or without the sack; `fine`: the + // 64x64 level, else the 32x32 one + pub fn hd_cell_view(k: EK, white: bool, view: u32, hack: Option, sack: bool, fine: bool) -> Option { let j = HD_SPRITES.iter().position(|h| h.kind == k)? as u32; 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; - 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 @@ -2582,36 +2637,46 @@ mod sprite_tests { use super::*; 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 fn differ(img: &Image, a: Rect, b: Rect) -> usize { - (0..ACELL * ACELL) + let n = a.w as u32; + (0..n * n) .filter(|i| { - let p = img.get_pixel(a.x as u32 + i % ACELL, a.y as u32 + i / ACELL); - let q = img.get_pixel(b.x as u32 + i % ACELL, b.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 % n, b.y as u32 + i / n); (p.r, p.g, p.b, p.a) != (q.r, q.g, q.b, q.a) }) .count() } #[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 { - for hack in [None, Some(0), Some(1), Some(2), Some(3)] { - let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false).expect("the imp has a detailed sprite"); - let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false).unwrap(); - let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true).unwrap(); - assert!(opaque(&img, r) > 150, "view {view} {hack:?} drawn"); - assert_eq!(opaque(&img, r), opaque(&img, w), "white silhouette matches"); - assert!(differ(&img, s, r) > 15, "view {view} {hack:?}: the sack shows"); + fn detailed_sprites() { + let img = gen_hd_sprites(); + assert_eq!((img.width as f32, img.height as f32), (HD_TEX_W, HD_TEX_H)); + for fine in [true, false] { + for view in 0..VIEWS { + for hack in [None, Some(0), Some(1), Some(2), Some(3)] { + let r = Gfx::hd_cell_view(EK::Worker, false, view, hack, false, fine).expect("the imp has a detailed sprite"); + let w = Gfx::hd_cell_view(EK::Worker, true, view, hack, false, fine).unwrap(); + let s = Gfx::hd_cell_view(EK::Worker, false, view, hack, true, fine).unwrap(); + assert!(opaque(&img, r) > 600, "view {view} {hack:?} {fine} drawn"); + 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()); - // the 10x10 sprites: every pixel a 3x3 block; the imp's sack variant + assert!(Gfx::hd_cell_view(EK::Knight, false, 0, None, false, true).is_none()); + } + + #[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); for y 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: - // rows: 10x10, 10x10 with sack, then idle and the hack frames without and with sack + // `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed cells to /tmp for + // a look: rows idle and the hack frames, without and with sack; 64x64 left, 32x32 right #[test] #[ignore] fn dump_imp_cells() { - let img = gen_sprites(); - let mut cells: Vec> = Vec::new(); - for sack in [false, true] { - let r: Vec = (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); - } + let img = gen_hd_sprites(); + let mut rows: Vec> = Vec::new(); for sack in [false, true] { 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 = (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 (w, h) = (ACELL * z * VIEWS, ACELL * z * cells.len() as u32); + let c = HD_CELL; + 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)); - for (row, list) in cells.iter().enumerate() { - for (view, r) in list.iter().enumerate() { - for y in 0..ACELL * z { - for x in 0..ACELL * z { - let p = img.get_pixel(r.x as u32 + x / z, r.y as u32 + y / z); + for (row, list) in rows.iter().enumerate() { + for (i, r) in list.iter().enumerate() { + for y in 0..c { + for x in 0..c { + let p = img.get_pixel(r.x as u32 + x, r.y as u32 + y); 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); } } } diff --git a/src/render3d.rs b/src/render3d.rs index a97884d..a889602 100644 --- a/src/render3d.rs +++ b/src/render3d.rs @@ -1563,8 +1563,9 @@ impl R3d { // sprites as billboards // 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); - if let Some(mesh) = self.billboards(g, gfx, cam, t, world_rows) { - draw_mesh(&mesh); + let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows); + for mesh in [small, detailed].iter().flatten() { + draw_mesh(mesh); } // solid unlit objects (default material, depth tested) gl_use_default_material(); @@ -1638,7 +1639,8 @@ impl R3d { self.draw_2d(g, gfx, cam, &m, t); } - fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Option { + // 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, Option) { let (_, r, _) = cam.basis(); let toward = cam.eye_dir(); let amb = |x: f32, z: f32| { @@ -1656,6 +1658,7 @@ impl R3d { let rg = vec2(r.x, r.z).normalize_or_zero(); let tg = vec2(toward.x, toward.z).normalize_or_zero(); let mut mb = MB::new(); + let mut mb_hd = MB::new(); let sw = SPRITE_TEX_W; let sh = SPRITE_TEX_H; // 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 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), // frame by frame with the same rhythm as the swing below 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 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 { // the frames swing the pickaxe: no tilting of the whole figure rot = 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 (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 u1, v1) = ((src.x + src.w) / sw, (src.y + src.h) / sh); if mirror { @@ -1813,6 +1821,7 @@ impl R3d { }; // 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 + let mb = if hd.is_some() { &mut mb_hd } else { &mut mb }; 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)], [vec2(u0, v0), vec2(u1, v0), vec2(u1, v1), vec2(u0, v1)], @@ -1820,11 +1829,8 @@ impl R3d { [toward; 4], ); } - if mb.v.is_empty() { - None - } else { - Some(Mesh { vertices: mb.v, indices: mb.i, texture: Some(gfx.sprites.clone()) }) - } + 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()) }) }; + (mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites)) } fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {