High-res textures take on the pixel-art colours; treasury and mushroom garden closer to the pixel art

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Richard ZinkandClaude Opus 5.5 committed 2026-11-07 22:34:20 +01:00
1 parent 2e905fe374
commit 9cba545bc7
19 files changed
+119 -6

No files matched your search

+106 -2
View File
@@ -458,6 +458,59 @@ pub fn hd_tile(id: u32, x: i32, z: i32) -> u32 {
}
}
// mean and standard deviation per channel of the opaque texels of some cells
fn cell_stats(img: &Image, cells: &[u32], tile: u32) -> ([f32; 3], [f32; 3]) {
let (mut sum, mut sq, mut n) = ([0f32; 3], [0f32; 3], 0f32);
for &id in cells {
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
let p = img.get_pixel(ox + x, oy + y);
if p.a < 0.5 {
continue;
}
for (c, v) in [p.r, p.g, p.b].into_iter().enumerate() {
sum[c] += v;
sq[c] += v * v;
}
n += 1.0;
}
}
}
let n = n.max(1.0);
let mean = sum.map(|s| s / n);
let mut sd = [0f32; 3];
for c in 0..3 {
sd[c] = (sq[c] / n - mean[c] * mean[c]).max(0.0).sqrt();
}
(mean, sd)
}
// moves the colours of the high-res cells `dst` towards those of the 16x16 cells `src`
fn match_colors(img: &mut Image, small: &Image, src: &[u32], dst: &[u32], tile: u32) {
const STRENGTH: f32 = 0.8;
let (pm, ps) = cell_stats(small, src, 16);
let (hm, hs) = cell_stats(img, dst, tile);
for &id in dst {
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
for y in 0..tile {
for x in 0..tile {
let p = img.get_pixel(ox + x, oy + y);
if p.a < 0.5 {
continue;
}
let mut q = [p.r, p.g, p.b];
for c in 0..3 {
let k = (ps[c] / hs[c].max(0.01)).clamp(0.6, 1.6);
let m = pm[c] + (q[c] - hm[c]) * k;
q[c] = (q[c] + (m - q[c]) * STRENGTH).clamp(0.0, 1.0);
}
img.set_pixel(ox + x, oy + y, Color::new(q[0], q[1], q[2], p.a));
}
}
}
}
// a high-res atlas with `tile` x `tile` texel cells (HD_TILE or HD_TILE2)
fn gen_hd_atlas(tile: u32) -> Image {
let small = gen_atlas();
@@ -515,6 +568,25 @@ fn gen_hd_atlas(tile: u32) -> Image {
}
}
}
// the generated pictures take on the colours of the 16x16 cells they replace (mean and
// contrast per channel, most of the way), so switching between the styles keeps the look
let range = |a: u32, n: u32| (a..a + n).collect::<Vec<u32>>();
let mut groups: Vec<(Vec<u32>, Vec<u32>)> = Vec::new();
for &(base, n, _, _) in HD_TILES.iter() {
groups.push((range(base, n), range(base, n)));
}
for &(first, n, own, _, _, _) in HD_SETS.iter() {
groups.push((range(first, n), range(own, 5)));
}
for &(base, n, _, _) in HD_VARS.iter() {
groups.push((range(base, n), range(base, n)));
}
for &(id, _, _) in HD_DECALS.iter().skip(2) {
groups.push((vec![id], vec![id]));
}
for (src, dst) in groups.iter() {
match_colors(&mut img, &small, src, dst, tile);
}
img
}
@@ -2877,6 +2949,20 @@ mod hd_atlas_tests {
let p = img.get_pixel(x, y);
(p.r, p.g, p.b, p.a)
};
// the colours are matched to the 16x16 cells (see match_colors), but a picture keeps its
// shapes: what is lighter in the source stays lighter in the atlas
let placed = |hd: &Image, ox: u32, oy: u32, src: &Image, sx: u32, sy: u32| {
let lum = |p: Color| p.r + p.g + p.b;
let pts = [(3, 5), (7, 40), (30, 9), (50, 50), (20, 60), (61, 2)];
for &(ax, ay) in pts.iter() {
for &(bx, by) in pts.iter() {
let (s1, s2) = (lum(src.get_pixel(sx + ax, sy + ay)), lum(src.get_pixel(sx + bx, sy + by)));
if s1 + 0.15 < s2 {
assert!(lum(hd.get_pixel(ox + ax, oy + ay)) <= lum(hd.get_pixel(ox + bx, oy + by)) + 0.02, "cell at {ox},{oy}");
}
}
}
};
// a cell without an own texture: the 16x16 one scaled up
let (cx, cy) = ((T_PUMPKIN % ATLAS_COLS) * 16, (T_PUMPKIN / ATLAS_COLS) * 16);
for (x, y) in [(0, 0), (5, 9), (15, 15)] {
@@ -2891,7 +2977,7 @@ mod hd_atlas_tests {
let id = base + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
let (sx, sy) = if n == 4 { ((v % 2) * tile, (v / 2) * tile) } else { (0, 0) };
assert_eq!(px(&hd, ox + 7, oy + 40), px(&src, sx + 7, sy + 40));
placed(&hd, ox, oy, &src, sx, sy);
}
}
// the sets: five tiles each in their own cells (inside the atlas)
@@ -2902,7 +2988,7 @@ mod hd_atlas_tests {
for v in 0..5 {
let id = own + v;
let (ox, oy) = ((id % ATLAS_COLS) * tile, (id / ATLAS_COLS) * tile);
assert_eq!(px(&hd, ox + 9, oy + 30), px(&src, v * tile + 9, 30));
placed(&hd, ox, oy, &src, v * tile, 0);
}
}
}
@@ -2972,6 +3058,24 @@ mod atlas_dump {
}
out.export_png("/tmp/atlas-cell.png");
}
// `cargo test --bins dump_atlases -- --ignored`: the 16x16 atlas scaled up and the 64x64
// high-res atlas side by side in /tmp/atlases.png (to compare the colours of the styles)
#[test]
#[ignore]
fn dump_atlases() {
let small = gen_atlas();
let hd = gen_hd_atlas(HD_TILE);
let (w, h) = (hd.width as u32, hd.height as u32);
let mut out = Image::gen_image_color((2 * w + 16) as u16, h as u16, BLACK);
for y in 0..h {
for x in 0..w {
out.set_pixel(x, h - 1 - y, small.get_pixel(x / 4, y / 4));
out.set_pixel(w + 16 + x, h - 1 - y, hd.get_pixel(x, y));
}
}
out.export_png("/tmp/atlases.png");
}
}
#[cfg(test)]