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:
1 parent
10af85fd95
commit
928c34e78b
7 files changed
+121
-18
No files matched your search
+99
-10
@@ -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();
|
||||
|
||||
+2
-2
@@ -478,7 +478,7 @@ fn apply_settings(s: &Settings, r3d: &mut R3d) {
|
||||
r3d.outlines = s.outlines;
|
||||
r3d.set_wall_detail(s.wall_subdivisions());
|
||||
r3d.max_lights = s.max_lights as usize;
|
||||
r3d.pixel_rows = if [270, 360, 480].contains(&s.pixel_rows) { s.pixel_rows as u32 } else { 360 };
|
||||
r3d.pixel_rows = settings::pixel_rows(s.pixel_rows);
|
||||
audio::set_volumes(s.music_volume, s.volume, s.muted);
|
||||
}
|
||||
|
||||
@@ -985,7 +985,7 @@ async fn main() {
|
||||
};
|
||||
ui::set_touch(touch_mode);
|
||||
// pixel look: the interface on a virtual screen whose pixels are whole parts of the world's
|
||||
let rows = if [270, 360, 480].contains(&app.settings.pixel_rows) { app.settings.pixel_rows as u32 } else { 360 };
|
||||
let rows = settings::pixel_rows(app.settings.pixel_rows);
|
||||
// the interface needs room: the smallest whole multiple of the world's rows with at least
|
||||
// 900 rows (360 -> 1080), on a desktop and on a touch screen alike (a phone has about
|
||||
// 1080 device rows in landscape; a smaller screen draws in device pixels)
|
||||
|
||||
+1
-1
@@ -200,7 +200,7 @@ fn item_change(it: Item, s: &mut Settings, d: i32) {
|
||||
Item::WallDetail => s.wall_detail = cycle(&[1u8, 2, 3], s.wall_detail, d),
|
||||
Item::Lights => s.max_lights = cycle(&[8u8, 16, 24], s.max_lights, d),
|
||||
Item::UiScale => s.ui_size = cycle(&crate::settings::UI_SIZES, s.ui_size, d),
|
||||
Item::RenderScale => s.pixel_rows = cycle(&[270u16, 360, 480], if [270, 360, 480].contains(&s.pixel_rows) { s.pixel_rows } else { 360 }, d),
|
||||
Item::RenderScale => s.pixel_rows = cycle(&crate::settings::PIXEL_ROWS, crate::settings::pixel_rows(s.pixel_rows) as u16, d),
|
||||
Item::DebugFps => s.debug_fps = !s.debug_fps,
|
||||
Item::DebugFrameTime => s.debug_frame_time = !s.debug_frame_time,
|
||||
Item::DebugPing => s.debug_ping = !s.debug_ping,
|
||||
|
||||
+10
-3
@@ -1561,7 +1561,9 @@ impl R3d {
|
||||
}
|
||||
}
|
||||
// sprites as billboards
|
||||
if let Some(mesh) = self.billboards(g, gfx, cam, t) {
|
||||
// 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);
|
||||
}
|
||||
// solid unlit objects (default material, depth tested)
|
||||
@@ -1636,7 +1638,7 @@ impl R3d {
|
||||
self.draw_2d(g, gfx, cam, &m, t);
|
||||
}
|
||||
|
||||
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32) -> Option<Mesh> {
|
||||
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Option<Mesh> {
|
||||
let (_, r, _) = cam.basis();
|
||||
let toward = cam.eye_dir();
|
||||
let amb = |x: f32, z: f32| {
|
||||
@@ -1656,6 +1658,9 @@ impl R3d {
|
||||
let mut mb = MB::new();
|
||||
let sw = SPRITE_TEX_W;
|
||||
let sh = SPRITE_TEX_H;
|
||||
// screen pixels per tile: a sprite's cell (1.2 sizes high) shows a detailed sprite when
|
||||
// every texel of it gets nearly a pixel; fewer pixels would only turn it into noise
|
||||
let px_per_tile = world_rows / cam.view_h().max(0.1);
|
||||
for e in &g.ents {
|
||||
if !e.alive || e.held || unit_hidden(g, e) {
|
||||
continue;
|
||||
@@ -1754,7 +1759,9 @@ impl R3d {
|
||||
lunge = 0.0;
|
||||
}
|
||||
let white = e.flash > 0.07;
|
||||
let src = Gfx::sprite_cell_view(e.kind, white, e.level, if e.captive != 0 { 0 } else { view });
|
||||
let v = if e.captive != 0 { 0 } else { view };
|
||||
let detailed = w * 1.2 * px_per_tile >= crate::gfx::ACELL as f32 * 0.85;
|
||||
let src = detailed.then(|| Gfx::hd_cell_view(e.kind, white, e.level, v)).flatten().unwrap_or_else(|| Gfx::sprite_cell_view(e.kind, white, e.level, v));
|
||||
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 {
|
||||
|
||||
+8
-1
@@ -151,7 +151,7 @@ pub struct Settings {
|
||||
pub ui_factor: u8, // old interface size (2 large, 3 medium, 4 small; only read to carry old settings over)
|
||||
pub ui_size: u16, // interface size in percent of the standard (100 = 1080 interface rows), 0 = automatic
|
||||
pub render_scale: u8, // old pixel-look divisor (no longer used, kept for old settings files)
|
||||
pub pixel_rows: u16, // pixel look: rows of the world on every screen (270, 360, 480)
|
||||
pub pixel_rows: u16, // pixel look: rows of the world on every screen (one of PIXEL_ROWS)
|
||||
pub touch_ui: u8, // touch interface: 0 = automatic (after the first touch), 1 = on, 2 = off
|
||||
pub debug_fps: bool, // show FPS and rendering backend
|
||||
pub debug_frame_time: bool, // show the frame-time line and graph
|
||||
@@ -264,6 +264,13 @@ impl Settings {
|
||||
}
|
||||
}
|
||||
|
||||
// the world's pixel rows of the settings menu (more rows: finer world, detailed sprites fit)
|
||||
pub const PIXEL_ROWS: [u16; 5] = [270, 360, 480, 540, 1080];
|
||||
|
||||
pub fn pixel_rows(set: u16) -> u32 {
|
||||
if PIXEL_ROWS.contains(&set) { set as u32 } else { 360 }
|
||||
}
|
||||
|
||||
// the interface sizes of the settings menu in percent (0 = automatic), from small to large
|
||||
pub const UI_SIZES: [u16; 9] = [0, 75, 85, 100, 115, 130, 150, 175, 200];
|
||||
|
||||
|
||||
Reference in new issue
Block a user