Detailed sprites: frames for walk and attack animations, gear laid over them, hit flash in the shader, 32x32 level in its own texture
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
01e5326721
commit
ffad92b641
2 files changed
+362
-126
No files matched your search
+255
-111
@@ -101,7 +101,9 @@ pub const T_EGG: u32 = 222; // 2 variants: a painted egg (Easter)
|
||||
pub struct Gfx {
|
||||
pub atlas: Texture2D,
|
||||
pub sprites: Texture2D,
|
||||
pub hd_sprites: Texture2D, // detailed sprites (see HD_SPRITES, hd_cell_view)
|
||||
pub hd_sprites: Texture2D, // detailed sprites, 64x64 level (see HD_SPRITES, hd_picture)
|
||||
pub hd_sprites32: Texture2D, // the same, 32x32 level
|
||||
pub gear: Texture2D, // their gear parts (see GEAR_HELMET etc.)
|
||||
pub atlas_hd: Texture2D, // the tile atlas of the high-res style, 64x64 tiles (see HD_TILES)
|
||||
pub atlas_hd2: Texture2D, // the same with 128x128 tiles (close up)
|
||||
pub materials: [Texture2D; 8], // furniture materials, high-res (see crate::props::Mat)
|
||||
@@ -1735,17 +1737,26 @@ const SPR_CELL: u32 = 12;
|
||||
const SPR_K: u32 = 3;
|
||||
pub const ACELL: u32 = SPR_CELL * SPR_K;
|
||||
|
||||
// Detailed sprites (generated, see art/generated/imp-sprite and art/generated/sprites) 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
|
||||
// imp): the digging animation, HACK_FRAMES frames side by side, one row per view. No gear yet.
|
||||
// Detailed sprites (generated, see art/generated/imp-sprite and art/generated/sprites), used
|
||||
// instead of the 10x10 ones in the high-res graphics style (render3d::billboards). Two levels of
|
||||
// detail from the same pictures, in two textures: 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); an animation:
|
||||
// ANIM_FRAMES frames side by side, one row per view. The gear (shoulder pieces, helmet, cape,
|
||||
// crown) is laid over them at draw time (see hd_anchor, GEAR_PARTS); the hit flash is drawn
|
||||
// white by the shader.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub enum HdAnim {
|
||||
Hack,
|
||||
Walk,
|
||||
Attack,
|
||||
}
|
||||
pub const ANIM_FRAMES: u32 = 4;
|
||||
struct HdSprite {
|
||||
kind: EK,
|
||||
idle: &'static [u8],
|
||||
idle_32: &'static [u8],
|
||||
hack: Option<(&'static [u8], &'static [u8])>, // 64x64 and 32x32
|
||||
anims: &'static [(HdAnim, &'static [u8], &'static [u8])], // 64x64 and 32x32 sheets
|
||||
}
|
||||
macro_rules! hd_sprite {
|
||||
($kind:expr, $name:literal) => {
|
||||
@@ -1753,7 +1764,7 @@ macro_rules! hd_sprite {
|
||||
kind: $kind,
|
||||
idle: include_bytes!(concat!("../assets/sprites/", $name, "-64.png")),
|
||||
idle_32: include_bytes!(concat!("../assets/sprites/", $name, "-32.png")),
|
||||
hack: None,
|
||||
anims: &[],
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1762,7 +1773,7 @@ const HD_SPRITES: [HdSprite; 31] = [
|
||||
kind: EK::Worker,
|
||||
idle: include_bytes!("../assets/sprites/imp-64.png"),
|
||||
idle_32: include_bytes!("../assets/sprites/imp-32.png"),
|
||||
hack: Some((include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))),
|
||||
anims: &[(HdAnim::Hack, include_bytes!("../assets/sprites/imp-hack-64.png"), include_bytes!("../assets/sprites/imp-hack-32.png"))],
|
||||
},
|
||||
hd_sprite!(EK::Kobold, "kobold"),
|
||||
hd_sprite!(EK::Spider, "spider"),
|
||||
@@ -1795,38 +1806,73 @@ const HD_SPRITES: [HdSprite; 31] = [
|
||||
hd_sprite!(EK::Ranger, "ranger"),
|
||||
hd_sprite!(EK::Inquisitor, "inquisitor"),
|
||||
];
|
||||
pub const HACK_FRAMES: u32 = 4;
|
||||
// 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
|
||||
// so the billboard is the same; the 64x64 picture sits where the 10x10 sprite would; the 32x32
|
||||
// level has cells of half the size
|
||||
pub const HD_CELL: u32 = SPR_CELL * 6;
|
||||
// columns of a sprite with the digging animation, per detail level: idle and the hack frames,
|
||||
// each without and with the sack; a sprite without it has one column per level
|
||||
const HD_COLS: u32 = (1 + HACK_FRAMES) * 2;
|
||||
// the texture: the sprites' columns one after another (a sprite's own columns: the 64x64 level,
|
||||
// then the 32x32 one), in bands of HD_BAND columns (a sprite never straddles two bands), every
|
||||
// band VIEWS * 2 rows (a view, its white silhouette)
|
||||
const HD_BAND: u32 = 40;
|
||||
const fn hd_width(j: usize) -> u32 {
|
||||
if HD_SPRITES[j].hack.is_some() { HD_COLS * 2 } else { 2 }
|
||||
}
|
||||
const fn hd_start(j: usize) -> u32 {
|
||||
let mut p = 0;
|
||||
let mut i = 0;
|
||||
while i < j {
|
||||
p += hd_width(i);
|
||||
i += 1;
|
||||
}
|
||||
p
|
||||
}
|
||||
const HD_BANDS: u32 = (hd_start(HD_SPRITES.len()) + HD_BAND - 1) / HD_BAND;
|
||||
pub const HD_TEX_W: f32 = (HD_BAND * HD_CELL) as f32;
|
||||
pub const HD_TEX_H: f32 = (HD_BANDS * VIEWS * 2 * HD_CELL) as f32;
|
||||
const HD_COLS64: u32 = 56;
|
||||
const HD_COLS32: u32 = 112;
|
||||
|
||||
// where column `c` of sprite j lies: (texture column, first row of its band)
|
||||
fn hd_place(j: usize, c: u32) -> (u32, u32) {
|
||||
let p = hd_start(j) + c;
|
||||
(p % HD_BAND, p / HD_BAND * VIEWS * 2)
|
||||
// the pictures one after another: per sprite its frames (idle, then every animation's frames),
|
||||
// per frame without and (kinds with a sack) with the sack, per those the five views
|
||||
fn hd_frames(j: usize) -> u32 {
|
||||
1 + HD_SPRITES[j].anims.len() as u32 * ANIM_FRAMES
|
||||
}
|
||||
fn hd_sacks(j: usize) -> u32 {
|
||||
if SACK_KINDS.contains(&HD_SPRITES[j].kind) { 2 } else { 1 }
|
||||
}
|
||||
fn hd_base(j: usize) -> u32 {
|
||||
(0..j).map(|i| hd_frames(i) * hd_sacks(i) * VIEWS).sum()
|
||||
}
|
||||
fn hd_pictures() -> u32 {
|
||||
hd_base(HD_SPRITES.len())
|
||||
}
|
||||
// the size of a detailed sprite texture (fine: the 64x64 one)
|
||||
pub fn hd_tex_size(fine: bool) -> (u32, u32) {
|
||||
let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) };
|
||||
(cols * cell, hd_pictures().div_ceil(cols) * cell)
|
||||
}
|
||||
|
||||
// where a picture's gear goes: rows and columns of a 72x72 cell as shares of it (0..1)
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct GearAnchor {
|
||||
pub top: f32, // top of the head
|
||||
pub bottom: f32, // the feet
|
||||
pub head: (f32, f32), // the head's left and right edge (a little below its top)
|
||||
pub shoulder: f32, // the shoulders' row
|
||||
pub sh: (f32, f32), // the shoulders' left and right edge
|
||||
}
|
||||
static HD_ANCHORS: std::sync::OnceLock<Vec<GearAnchor>> = std::sync::OnceLock::new();
|
||||
|
||||
fn gear_anchor(g: &Cell) -> GearAnchor {
|
||||
let n = g.len();
|
||||
let f = |v: usize| v as f32 / n as f32;
|
||||
let span = |y: usize| -> Option<(usize, usize)> {
|
||||
let xs: Vec<usize> = (0..n).filter(|&x| g[y][x].is_some()).collect();
|
||||
Some((*xs.first()?, *xs.last()?))
|
||||
};
|
||||
let rows: Vec<usize> = (0..n).filter(|&y| span(y).is_some()).collect();
|
||||
let (Some(&top), Some(&bot)) = (rows.first(), rows.last()) else { return GearAnchor::default() };
|
||||
let h = bot - top + 1;
|
||||
let head_y = (top + h / 10).min(bot);
|
||||
let head = span(head_y).unwrap_or((n / 3, 2 * n / 3));
|
||||
// the shoulders: the widest row of the upper body (tools and wings stick out: the middle
|
||||
// row of the widest few counts)
|
||||
let (a, b) = (top + h * 3 / 10, (top + h / 2).min(bot));
|
||||
let shoulder = (a..=b).max_by_key(|&y| span(y).map_or(0, |(l, r)| r - l)).unwrap_or(a);
|
||||
let sh = span(shoulder).unwrap_or(head);
|
||||
GearAnchor { top: f(top), bottom: f(bot + 1), head: (f(head.0), f(head.1 + 1)), shoulder: f(shoulder), sh: (f(sh.0), f(sh.1 + 1)) }
|
||||
}
|
||||
|
||||
// the gear's parts in their texture (grey, tinted by the metal or cloth colour at draw time):
|
||||
// (x, y, w, h) in texels of GEAR_TEX
|
||||
pub const GEAR_TEX: u32 = 64;
|
||||
pub const GEAR_HELMET: (u32, u32, u32, u32) = (0, 0, 24, 12);
|
||||
pub const GEAR_PAULDRON: (u32, u32, u32, u32) = (24, 0, 12, 9);
|
||||
pub const GEAR_CROWN: (u32, u32, u32, u32) = (36, 0, 20, 10);
|
||||
pub const GEAR_CAPE: (u32, u32, u32, u32) = (0, 14, 28, 40);
|
||||
pub const GEAR_HORN: (u32, u32, u32, u32) = (28, 14, 7, 10);
|
||||
pub const GEAR_PLUME: (u32, u32, u32, u32) = (36, 14, 7, 12);
|
||||
|
||||
// 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
|
||||
@@ -2637,36 +2683,96 @@ fn gen_sprites() -> Image {
|
||||
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));
|
||||
// a picture into a texture of `cell` x `cell` cells, `cols` per row (no white silhouette:
|
||||
// the shader flashes the detailed sprites)
|
||||
fn put_picture(img: &mut Image, pic: u32, cols: u32, cell: u32, g: &Cell) {
|
||||
let (ox, oy) = ((pic % cols) * cell, (pic / cols) * cell);
|
||||
for (y, line) in g.iter().enumerate() {
|
||||
for (x, p) in line.iter().enumerate() {
|
||||
if let Some(c) = p {
|
||||
img.set_pixel(ox + x as u32, oy + y as u32, *c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// the two textures of the detailed sprites (64x64 level, 32x32 level); the gear anchors of every
|
||||
// picture are measured on the way
|
||||
fn gen_hd_sprites() -> (Image, Image) {
|
||||
let (w64, h64) = hd_tex_size(true);
|
||||
let (w32, h32) = hd_tex_size(false);
|
||||
let clear = Color::new(0.0, 0.0, 0.0, 0.0);
|
||||
let mut fine = Image::gen_image_color(w64 as u16, h64 as u16, clear);
|
||||
let mut coarse = Image::gen_image_color(w32 as u16, h32 as u16, clear);
|
||||
let mut anchors = vec![GearAnchor::default(); hd_pictures() as usize];
|
||||
let png = |b: &[u8]| Image::from_file_with_format(b, Some(ImageFormat::Png)).expect("hd sprite");
|
||||
for (j, hd) in HD_SPRITES.iter().enumerate() {
|
||||
// (idle, hack, picture size, cell grid, texels per pixel, sack, where the sack shows from the front)
|
||||
let levels = [
|
||||
(png(hd.idle), hd.hack.map(|h| png(h.0)), 64, HD_CELL, 1, &SACK_64[..], (0.7, -0.65)),
|
||||
(png(hd.idle_32), hd.hack.map(|h| png(h.1)), 32, HD_CELL / 2, 2, &SACK_32[..], (0.7, -0.65)),
|
||||
];
|
||||
for (l, (idle, hack, n, size, k, sack, dx)) in levels.iter().enumerate() {
|
||||
let sheets64: Vec<Image> = std::iter::once(png(hd.idle)).chain(hd.anims.iter().map(|a| png(a.1))).collect();
|
||||
let sheets32: Vec<Image> = std::iter::once(png(hd.idle_32)).chain(hd.anims.iter().map(|a| png(a.2))).collect();
|
||||
let sacks = hd_sacks(j);
|
||||
for frame in 0..hd_frames(j) {
|
||||
// the frame's sheet and its place there: idle (frame 0) or frame k of an animation
|
||||
let (sheet, fx) = if frame == 0 { (0, None) } else { (1 + ((frame - 1) / ANIM_FRAMES) as usize, Some((frame - 1) % ANIM_FRAMES)) };
|
||||
for view in 0..VIEWS {
|
||||
let Some(hack) = hack else {
|
||||
let (col, row) = hd_place(j, l as u32);
|
||||
put_cell(&mut img, col, row + view * 2, &hd_cell(idle, view * n, 0, *n, *size), *k, HD_CELL);
|
||||
continue;
|
||||
let at = |n: u32| match fx {
|
||||
None => (view * n, 0),
|
||||
Some(f) => (f * n, view * n),
|
||||
};
|
||||
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, row) = hd_place(j, l as u32 * HD_COLS + f * 2);
|
||||
put_cell(&mut img, col, row + view * 2, &g, *k, HD_CELL);
|
||||
let mut s = g;
|
||||
add_sack(&mut s, view, sack, *dx);
|
||||
put_cell(&mut img, col + 1, row + view * 2, &s, *k, HD_CELL);
|
||||
let (x64, y64) = at(64);
|
||||
let (x32, y32) = at(32);
|
||||
let g64 = hd_cell(&sheets64[sheet], x64, y64, 64, HD_CELL);
|
||||
let g32 = hd_cell(&sheets32[sheet], x32, y32, 32, HD_CELL / 2);
|
||||
for sack in 0..sacks {
|
||||
let pic = hd_base(j) + (frame * sacks + sack) * VIEWS + view;
|
||||
let (mut a, mut b) = (g64.clone(), g32.clone());
|
||||
if sack == 1 {
|
||||
add_sack(&mut a, view, &SACK_64, (0.7, -0.65));
|
||||
add_sack(&mut b, view, &SACK_32, (0.7, -0.65));
|
||||
}
|
||||
anchors[pic as usize] = gear_anchor(&g64);
|
||||
put_picture(&mut fine, pic, HD_COLS64, HD_CELL, &a);
|
||||
put_picture(&mut coarse, pic, HD_COLS32, HD_CELL / 2, &b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = HD_ANCHORS.set(anchors);
|
||||
(fine, coarse)
|
||||
}
|
||||
|
||||
// the gear parts (see GEAR_HELMET etc.): grey with dark outlines, tinted at draw time
|
||||
fn gen_gear() -> Image {
|
||||
let mut img = Image::gen_image_color(GEAR_TEX as u16, GEAR_TEX as u16, Color::new(0.0, 0.0, 0.0, 0.0));
|
||||
let grey = |v: f32| Color::new(v, v, v, 1.0);
|
||||
let outline = Color::new(0.08, 0.07, 0.09, 1.0);
|
||||
// a shape given by an inside test, outlined, lit from the upper left
|
||||
let mut shape = |(ox, oy, w, h): (u32, u32, u32, u32), inside: &dyn Fn(f32, f32) -> bool, shade: &dyn Fn(f32, f32) -> f32| {
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let (fx, fy) = ((x as f32 + 0.5) / w as f32, (y as f32 + 0.5) / h as f32);
|
||||
if !inside(fx, fy) {
|
||||
continue;
|
||||
}
|
||||
let edge = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)].iter().any(|&(dx, dy)| {
|
||||
let (nx, ny) = (x as i32 + dx, y as i32 + dy);
|
||||
nx < 0 || ny < 0 || nx >= w as i32 || ny >= h as i32 || !inside((nx as f32 + 0.5) / w as f32, (ny as f32 + 0.5) / h as f32)
|
||||
});
|
||||
img.set_pixel(ox + x, oy + y, if edge { outline } else { grey(shade(fx, fy)) });
|
||||
}
|
||||
}
|
||||
};
|
||||
// helmet: a dome with a rim
|
||||
shape(GEAR_HELMET, &|x, y| (x - 0.5).powi(2) * 4.0 + (1.0 - y).powi(2) <= 1.0, &|x, y| if y > 0.75 { 0.55 } else { 0.95 - (x * 0.35 + y * 0.25) });
|
||||
// shoulder piece: a rounded plate with a rivet
|
||||
shape(GEAR_PAULDRON, &|x, y| (x - 0.5).powi(2) * 4.0 + (y - 0.6).powi(2) * 2.6 <= 1.0, &|x, y| if (x - 0.5).abs() < 0.1 && (y - 0.5).abs() < 0.12 { 1.0 } else { 0.9 - y * 0.4 });
|
||||
// crown: a band with three spikes
|
||||
shape(GEAR_CROWN, &|x, y| y > 0.55 || ((x * 3.0).fract() - 0.5).abs() < y * 0.55, &|x, y| if y > 0.6 && (x * 5.0).fract() < 0.25 { 0.6 } else { 1.0 - x * 0.2 });
|
||||
// cape: wider at the hem, with folds
|
||||
shape(GEAR_CAPE, &|x, y| (x - 0.5).abs() < 0.32 + 0.18 * y, &|x, y| 0.8 - (((x - 0.5) * 9.0 + y).sin().abs()) * 0.25 - y * 0.15);
|
||||
// horn: a curved spike (bone)
|
||||
shape(GEAR_HORN, &|x, y| (x - (1.0 - y) * 0.4 - 0.3).abs() < 0.16 + 0.3 * y, &|x, y| 0.95 - y * 0.25 - x * 0.1);
|
||||
// plume: a feather bush
|
||||
shape(GEAR_PLUME, &|x, y| (x - 0.5).powi(2) * 3.2 + (y - 0.5).powi(2) * 1.1 <= 0.26 + 0.04 * (y * 14.0).sin(), &|_x, y| 0.95 - y * 0.3);
|
||||
img
|
||||
}
|
||||
|
||||
@@ -2794,8 +2900,13 @@ impl Gfx {
|
||||
atlas_hd.set_filter(FilterMode::Nearest);
|
||||
let atlas_hd2 = Texture2D::from_image(&gen_hd_atlas(HD_TILE2));
|
||||
atlas_hd2.set_filter(FilterMode::Nearest);
|
||||
let hd_sprites = Texture2D::from_image(&gen_hd_sprites());
|
||||
let (hd64, hd32) = gen_hd_sprites();
|
||||
let hd_sprites = Texture2D::from_image(&hd64);
|
||||
hd_sprites.set_filter(FilterMode::Nearest);
|
||||
let hd_sprites32 = Texture2D::from_image(&hd32);
|
||||
hd_sprites32.set_filter(FilterMode::Nearest);
|
||||
let gear = Texture2D::from_image(&gen_gear());
|
||||
gear.set_filter(FilterMode::Nearest);
|
||||
let mat = |b: &[u8]| {
|
||||
let t = Texture2D::from_file_with_format(b, Some(ImageFormat::Png));
|
||||
t.set_filter(FilterMode::Nearest);
|
||||
@@ -2803,7 +2914,7 @@ impl Gfx {
|
||||
};
|
||||
let materials = MATERIALS.map(|(hd, _)| mat(hd));
|
||||
let materials_px = MATERIALS.map(|(_, px)| mat(px));
|
||||
Gfx { atlas, sprites, hd_sprites, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, font, font_map }
|
||||
Gfx { atlas, sprites, hd_sprites, hd_sprites32, gear, atlas_hd, atlas_hd2, materials, materials_px, cursors, badges, font, font_map }
|
||||
}
|
||||
|
||||
pub fn cell(id: u32) -> Rect {
|
||||
@@ -2830,21 +2941,34 @@ 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 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<u32>, sack: bool, fine: bool) -> Option<Rect> {
|
||||
// the picture of a detailed sprite, if the kind has one: a view, standing or a frame of an
|
||||
// animation (an animation the kind does not have: standing), with or without the sack
|
||||
pub fn hd_picture(k: EK, view: u32, anim: Option<(HdAnim, u32)>, sack: bool) -> Option<u32> {
|
||||
let j = HD_SPRITES.iter().position(|h| h.kind == k)?;
|
||||
// sprites without the digging animation (and without a sack) show their idle picture
|
||||
let c = if HD_SPRITES[j].hack.is_some() {
|
||||
let f = hack.map_or(0, |f| 1 + f % HACK_FRAMES);
|
||||
(!fine as u32) * HD_COLS + f * 2 + sack as u32
|
||||
} else {
|
||||
!fine as u32
|
||||
};
|
||||
let (col, band) = hd_place(j, c);
|
||||
let row = band + view.min(VIEWS - 1) * 2 + white as u32;
|
||||
Some(Rect::new((col * HD_CELL) as f32, (row * HD_CELL) as f32, HD_CELL as f32, HD_CELL as f32))
|
||||
let frame = anim
|
||||
.and_then(|(a, f)| HD_SPRITES[j].anims.iter().position(|x| x.0 == a).map(|i| 1 + i as u32 * ANIM_FRAMES + f % ANIM_FRAMES))
|
||||
.unwrap_or(0);
|
||||
let sacks = hd_sacks(j);
|
||||
let sack = if sacks > 1 { sack as u32 } else { 0 };
|
||||
Some(hd_base(j) + (frame * sacks + sack) * VIEWS + view.min(VIEWS - 1))
|
||||
}
|
||||
|
||||
// whether a kind has a drawn animation
|
||||
pub fn hd_has_anim(k: EK, anim: HdAnim) -> bool {
|
||||
HD_SPRITES.iter().any(|h| h.kind == k && h.anims.iter().any(|a| a.0 == anim))
|
||||
}
|
||||
|
||||
// a picture's place in its texture as texture coordinates (fine: hd_sprites, else hd_sprites32)
|
||||
pub fn hd_uv(pic: u32, fine: bool) -> Rect {
|
||||
let (cols, cell) = if fine { (HD_COLS64, HD_CELL) } else { (HD_COLS32, HD_CELL / 2) };
|
||||
let (w, h) = hd_tex_size(fine);
|
||||
let (x, y) = ((pic % cols) * cell, (pic / cols) * cell);
|
||||
Rect::new(x as f32 / w as f32, y as f32 / h as f32, cell as f32 / w as f32, cell as f32 / h as f32)
|
||||
}
|
||||
|
||||
// where the gear of a picture goes (measured when the textures were made)
|
||||
pub fn hd_anchor(pic: u32) -> GearAnchor {
|
||||
HD_ANCHORS.get().and_then(|a| a.get(pic as usize).copied()).unwrap_or_default()
|
||||
}
|
||||
|
||||
// whether a kind carries its treasure in a sack on the back
|
||||
@@ -3231,11 +3355,6 @@ mod atlas_dump {
|
||||
mod sprite_tests {
|
||||
use super::*;
|
||||
|
||||
fn opaque(img: &Image, r: Rect) -> usize {
|
||||
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 {
|
||||
let n = a.w as u32;
|
||||
@@ -3248,35 +3367,50 @@ mod sprite_tests {
|
||||
.count()
|
||||
}
|
||||
|
||||
// texels of a picture that differ with and without something (pictures of the 64x64 level)
|
||||
fn pic_texels(img: &Image, pic: u32) -> Vec<(u8, u8, u8, u8)> {
|
||||
let (ox, oy) = ((pic % HD_COLS64) * HD_CELL, (pic / HD_COLS64) * HD_CELL);
|
||||
(0..HD_CELL * HD_CELL)
|
||||
.map(|i| {
|
||||
let p = img.get_pixel(ox + i % HD_CELL, oy + i / HD_CELL);
|
||||
((p.r * 255.0) as u8, (p.g * 255.0) as u8, (p.b * 255.0) as u8, (p.a * 255.0) as u8)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
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] {
|
||||
let (fine, coarse) = gen_hd_sprites();
|
||||
assert_eq!((fine.width as u32, fine.height as u32), hd_tex_size(true));
|
||||
assert_eq!((coarse.width as u32, coarse.height as u32), hd_tex_size(false));
|
||||
assert!(fine.width <= 4096 && fine.height <= 4096, "the texture fits every GPU");
|
||||
// the imp: idle and the digging frames, with and without the sack
|
||||
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");
|
||||
for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 3))] {
|
||||
let p = Gfx::hd_picture(EK::Worker, view, anim, false).unwrap();
|
||||
let s = Gfx::hd_picture(EK::Worker, view, anim, true).unwrap();
|
||||
let (a, b) = (pic_texels(&fine, p), pic_texels(&fine, s));
|
||||
assert!(a.iter().filter(|t| t.3 > 128).count() > 600, "view {view} {anim:?} drawn");
|
||||
assert!(a.iter().zip(b.iter()).filter(|(x, y)| x != y).count() > 60, "view {view} {anim:?}: the sack shows");
|
||||
}
|
||||
}
|
||||
}
|
||||
// every kind has a detailed sprite, none straddles two bands of the texture
|
||||
assert!(Gfx::hd_has_anim(EK::Worker, HdAnim::Hack));
|
||||
// every kind has its pictures; an animation it lacks shows it standing
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for &k in SPRITE_KINDS.iter() {
|
||||
for fine in [true, false] {
|
||||
for view in 0..VIEWS {
|
||||
let r = Gfx::hd_cell_view(k, false, view, None, false, fine).expect("a detailed sprite for every kind");
|
||||
assert!(opaque(&img, r) > 100, "{k:?} view {view} {fine}");
|
||||
assert!(r.x + r.w <= HD_TEX_W && r.y + r.h <= HD_TEX_H);
|
||||
let p = Gfx::hd_picture(k, view, None, false).expect("a detailed sprite for every kind");
|
||||
assert!(seen.insert(p), "{k:?}: pictures do not overlap");
|
||||
assert!(pic_texels(&fine, p).iter().filter(|t| t.3 > 128).count() > 100, "{k:?} view {view}");
|
||||
let a = Gfx::hd_anchor(p);
|
||||
assert!(a.top < a.shoulder && a.shoulder < a.bottom && a.sh.0 < a.sh.1, "{k:?} view {view}: {a:?}");
|
||||
if !Gfx::hd_has_anim(k, HdAnim::Walk) {
|
||||
assert_eq!(Gfx::hd_picture(k, view, Some((HdAnim::Walk, 2)), false), Some(p));
|
||||
}
|
||||
let uv = Gfx::hd_uv(p, false);
|
||||
assert!(uv.x + uv.w <= 1.0 + 1e-6 && uv.y + uv.h <= 1.0 + 1e-6);
|
||||
}
|
||||
}
|
||||
for j in 0..HD_SPRITES.len() {
|
||||
assert_eq!(hd_start(j) / HD_BAND, (hd_start(j) + hd_width(j) - 1) / HD_BAND, "sprite {j} straddles two bands");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3300,28 +3434,27 @@ mod sprite_tests {
|
||||
}
|
||||
}
|
||||
|
||||
// `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
|
||||
// `cargo test --bins dump_imp_cells -- --ignored` writes the imp's detailed pictures (idle
|
||||
// and the digging frames, without and with sack; the 64x64 level) and the gear parts to /tmp
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn dump_imp_cells() {
|
||||
let img = gen_hd_sprites();
|
||||
let mut rows: Vec<Vec<Rect>> = Vec::new();
|
||||
let (fine, _) = gen_hd_sprites();
|
||||
let mut rows: Vec<Vec<u32>> = Vec::new();
|
||||
for sack in [false, true] {
|
||||
for hack in [None, Some(0), Some(1), Some(2), Some(3)] {
|
||||
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);
|
||||
for anim in [None, Some((HdAnim::Hack, 0)), Some((HdAnim::Hack, 1)), Some((HdAnim::Hack, 2)), Some((HdAnim::Hack, 3))] {
|
||||
rows.push((0..VIEWS).map(|v| Gfx::hd_picture(EK::Worker, v, anim, sack).unwrap()).collect());
|
||||
}
|
||||
}
|
||||
let c = HD_CELL;
|
||||
let (w, h) = (c * VIEWS * 2, c * rows.len() as u32);
|
||||
let (w, h) = (c * VIEWS, 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 rows.iter().enumerate() {
|
||||
for (i, r) in list.iter().enumerate() {
|
||||
for (i, &pic) in list.iter().enumerate() {
|
||||
let (ox, oy) = ((pic % HD_COLS64) * c, (pic / HD_COLS64) * c);
|
||||
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);
|
||||
let p = fine.get_pixel(ox + x, oy + y);
|
||||
if p.a > 0.5 {
|
||||
out.set_pixel(i as u32 * c + x, h - 1 - (row as u32 * c + y), p);
|
||||
}
|
||||
@@ -3330,6 +3463,17 @@ mod sprite_tests {
|
||||
}
|
||||
}
|
||||
out.export_png("/tmp/imp-cells.png");
|
||||
let gear = gen_gear();
|
||||
let mut g = Image::gen_image_color((GEAR_TEX * 4) as u16, (GEAR_TEX * 4) as u16, Color::new(0.35, 0.33, 0.38, 1.0));
|
||||
for y in 0..GEAR_TEX * 4 {
|
||||
for x in 0..GEAR_TEX * 4 {
|
||||
let p = gear.get_pixel(x / 4, y / 4);
|
||||
if p.a > 0.5 {
|
||||
g.set_pixel(x, GEAR_TEX * 4 - 1 - y, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
g.export_png("/tmp/gear.png");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+107
-15
@@ -16,6 +16,8 @@ pub const MAX_LIGHTS: usize = 24;
|
||||
const ATLAS_W: f32 = 256.0;
|
||||
const ATLAS_H: f32 = (crate::gfx::ATLAS_ROWS * 16) as f32;
|
||||
const DISP_AMP: f32 = 0.11; // wall surface irregularity
|
||||
// vertex alpha of a detailed sprite flashing white after a hit (see the lit shader)
|
||||
const FLASH_A: u8 = 204;
|
||||
|
||||
// ------------------------------------------------------------------ camera
|
||||
|
||||
@@ -294,6 +296,12 @@ void main() {
|
||||
wave = dot(slope, vec2(0.45, 0.35)) * 0.6 + ripple * 0.4;
|
||||
}
|
||||
vec4 tex = texture2D(Texture, tuv);
|
||||
// the hit flash of the detailed sprites (vertex alpha FLASH_A): the picture in white
|
||||
if (color.a > 0.77 && color.a < 0.83) {
|
||||
if (tex.a < 0.5) discard;
|
||||
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
|
||||
return;
|
||||
}
|
||||
if (liquid && HdLiquid > 0.5) {
|
||||
// a second, larger layer drifting slower and sideways: the surface churns instead of
|
||||
// only sliding along
|
||||
@@ -1654,8 +1662,7 @@ 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);
|
||||
let (small, detailed) = self.billboards(g, gfx, cam, t, world_rows);
|
||||
for mesh in [small, detailed].iter().flatten() {
|
||||
for mesh in self.billboards(g, gfx, cam, t, world_rows).iter() {
|
||||
draw_mesh(mesh);
|
||||
}
|
||||
// solid unlit objects (default material, depth tested)
|
||||
@@ -1732,8 +1739,8 @@ impl R3d {
|
||||
self.draw_2d(g, gfx, cam, &m, t);
|
||||
}
|
||||
|
||||
// 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>) {
|
||||
// the units as billboards: the 10x10 sprites, the detailed ones (two levels) and their gear
|
||||
fn billboards(&self, g: &Game, gfx: &Gfx, cam: &Cam, t: f32, world_rows: f32) -> Vec<Mesh> {
|
||||
let (_, r, _) = cam.basis();
|
||||
let toward = cam.eye_dir();
|
||||
let amb = |x: f32, z: f32| {
|
||||
@@ -1752,6 +1759,8 @@ impl R3d {
|
||||
let tg = vec2(toward.x, toward.z).normalize_or_zero();
|
||||
let mut mb = MB::new();
|
||||
let mut mb_hd = MB::new();
|
||||
let mut mb_hd32 = MB::new();
|
||||
let mut mb_gear = 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
|
||||
@@ -1865,18 +1874,40 @@ impl R3d {
|
||||
// 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);
|
||||
// drawn animations (detailed sprites): digging, walking, attacking, frame by frame in
|
||||
// the rhythm of the swings below (two steps per walk cycle, as the bobbing)
|
||||
let cycle = |p: f32| ((p / std::f32::consts::TAU).rem_euclid(1.0) * crate::gfx::ANIM_FRAMES as f32) as u32;
|
||||
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, fine) } else { None };
|
||||
if hd.is_some() && hacking {
|
||||
// the frames swing the pickaxe: no tilting of the whole figure
|
||||
let attacking = e.working && !e.moving && !sleeping && matches!(e.job, Job::Attack(_) | Job::AttackTower(_) | Job::AttackCore | Job::Torment(_));
|
||||
let anim = if e.captive != 0 || sleeping {
|
||||
None
|
||||
} else if hacking {
|
||||
Some((crate::gfx::HdAnim::Hack, cycle(ph * 1.3)))
|
||||
} else if e.moving {
|
||||
Some((crate::gfx::HdAnim::Walk, cycle(e.anim * 12.0)))
|
||||
} else if attacking {
|
||||
Some((crate::gfx::HdAnim::Attack, cycle(ph * 0.9)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let pic = if detailed { Gfx::hd_picture(e.kind, v, anim, sack) } else { None };
|
||||
if let (Some(_), Some((a, _))) = (pic, anim) {
|
||||
if Gfx::hd_has_anim(e.kind, a) {
|
||||
// the frames move the figure: no tilting, lunging or bobbing of the whole
|
||||
rot = 0.0;
|
||||
lunge = 0.0;
|
||||
bob = bob.min(0.02);
|
||||
}
|
||||
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);
|
||||
}
|
||||
let src = match pic {
|
||||
Some(p) => Gfx::hd_uv(p, fine),
|
||||
None => {
|
||||
let r = 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));
|
||||
Rect::new(r.x / sw, r.y / sh, r.w / sw, r.h / sh)
|
||||
}
|
||||
};
|
||||
let (mut u0, v0) = (src.x, src.y);
|
||||
let (mut u1, v1) = (src.x + src.w, src.y + src.h);
|
||||
if mirror {
|
||||
std::mem::swap(&mut u0, &mut u1);
|
||||
}
|
||||
@@ -1884,7 +1915,9 @@ impl R3d {
|
||||
// spell looks: stone grey, plague green, see-through when invisible
|
||||
let alpha = if e.invis > 0.0 { 0.35 } else { 1.0 };
|
||||
let c = if white {
|
||||
[255, 255, 255, 255]
|
||||
// the detailed sprites flash white in the shader (FLASH_A), the 10x10 ones have
|
||||
// white silhouettes
|
||||
[255, 255, 255, if pic.is_some() { FLASH_A } else { 255 }]
|
||||
} else if e.captive == 3 {
|
||||
col(vec3(a * 1.2, a * 0.55, a * 0.55), 1.0)
|
||||
} else if e.shield > 0.0 {
|
||||
@@ -1914,7 +1947,66 @@ 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 };
|
||||
// the gear of the detailed sprites: parts laid over the picture where its anchors say
|
||||
// (shoulder pieces from level 3, a helmet from 5, a cape from 7, gold and a crown
|
||||
// from 9, like the 10x10 sprites' gear)
|
||||
if let Some(p) = pic {
|
||||
let tier = crate::gfx::gear_tier(e.level);
|
||||
if tier > 0 && !white && e.captive == 0 && !sleeping {
|
||||
let an = Gfx::hd_anchor(p);
|
||||
let shade = |rgb: (f32, f32, f32)| {
|
||||
let k = a * 1.25;
|
||||
[(rgb.0 * k).min(255.0) as u8, (rgb.1 * k).min(255.0) as u8, (rgb.2 * k).min(255.0) as u8, 255u8]
|
||||
};
|
||||
let metal = shade(match tier {
|
||||
1 => (196.0, 126.0, 60.0),
|
||||
4 => (250.0, 206.0, 72.0),
|
||||
_ => (208.0, 214.0, 228.0),
|
||||
});
|
||||
// a point of the picture's cell (shares of it) in the world
|
||||
let at = |fx: f32, fy: f32, depth: f32| {
|
||||
let fx = if mirror { 1.0 - fx } else { fx };
|
||||
corner((fx - 0.5) * 1.2 * w, (1.0 - fy) * 1.2 * h - 0.1 * h) + toward * depth
|
||||
};
|
||||
let gt = crate::gfx::GEAR_TEX as f32;
|
||||
let mut part = |x0: f32, y0: f32, x1: f32, y1: f32, uv: (u32, u32, u32, u32), c: [u8; 4], depth: f32, flip: bool| {
|
||||
let (mut a0, b0) = (uv.0 as f32 / gt, uv.1 as f32 / gt);
|
||||
let (mut a1, b1) = ((uv.0 + uv.2) as f32 / gt, (uv.1 + uv.3) as f32 / gt);
|
||||
if mirror != flip {
|
||||
std::mem::swap(&mut a0, &mut a1);
|
||||
}
|
||||
mb_gear.quad([at(x0, y0, depth), at(x1, y0, depth), at(x1, y1, depth), at(x0, y1, depth)], [vec2(a0, b0), vec2(a1, b0), vec2(a1, b1), vec2(a0, b1)], [c; 4], [toward; 4]);
|
||||
};
|
||||
let mid = (an.head.0 + an.head.1) * 0.5;
|
||||
let neck = an.top + (an.shoulder - an.top) * 0.45;
|
||||
if tier >= 3 {
|
||||
// the cape hangs behind the figure (seen from behind: over its back)
|
||||
let cape = shade(if e.hero { (44.0, 74.0, 156.0) } else { (128.0, 22.0, 34.0) });
|
||||
let depth = if v >= 3 { 0.004 } else { -0.004 };
|
||||
part(an.sh.0 - 0.03, an.shoulder - 0.02, an.sh.1 + 0.03, an.bottom - 0.1, crate::gfx::GEAR_CAPE, cape, depth, false);
|
||||
}
|
||||
if tier >= 2 {
|
||||
part(an.head.0 - 0.02, an.top - 0.03, an.head.1 + 0.02, neck, crate::gfx::GEAR_HELMET, metal, 0.006, false);
|
||||
if tier >= 4 {
|
||||
part(mid - 0.1, an.top - 0.11, mid + 0.1, an.top + 0.01, crate::gfx::GEAR_CROWN, shade((255.0, 220.0, 90.0)), 0.008, false);
|
||||
} else if e.hero {
|
||||
part(mid - 0.035, an.top - 0.13, mid + 0.035, an.top + 0.01, crate::gfx::GEAR_PLUME, shade((210.0, 40.0, 40.0)), 0.008, false);
|
||||
} else {
|
||||
let bone = shade((232.0, 222.0, 200.0));
|
||||
part(an.head.0 - 0.06, an.top - 0.11, an.head.0 + 0.02, an.top + 0.02, crate::gfx::GEAR_HORN, bone, 0.008, false);
|
||||
part(an.head.1 - 0.02, an.top - 0.11, an.head.1 + 0.06, an.top + 0.02, crate::gfx::GEAR_HORN, bone, 0.008, true);
|
||||
}
|
||||
}
|
||||
for (x, flip) in [(an.sh.0 + 0.03, false), (an.sh.1 - 0.03, true)] {
|
||||
part(x - 0.075, an.shoulder - 0.05, x + 0.075, an.shoulder + 0.06, crate::gfx::GEAR_PAULDRON, metal, 0.007, flip);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mb = match pic {
|
||||
Some(_) if fine => &mut mb_hd,
|
||||
Some(_) => &mut mb_hd32,
|
||||
None => &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)],
|
||||
@@ -1923,7 +2015,7 @@ impl R3d {
|
||||
);
|
||||
}
|
||||
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))
|
||||
[mesh(mb, &gfx.sprites), mesh(mb_hd, &gfx.hd_sprites), mesh(mb_hd32, &gfx.hd_sprites32), mesh(mb_gear, &gfx.gear)].into_iter().flatten().collect()
|
||||
}
|
||||
|
||||
fn draw_2d(&self, g: &Game, gfx: &Gfx, cam: &Cam, m: &Mat4, t: f32) {
|
||||
|
||||
Reference in new issue
Block a user