#!/usr/bin/env python3 """Turn a generated texture into a seamless pixel-art tile sheet for the high-res atlas. Usage: make_tile.py raw.png out_prefix [--size 128] [--colors 32] [--band 14] The picture is scaled down to size x size (default 128: a 2x2 block of 64x64 tiles; the game lays the four quarters out by the tiles' positions, so the floor repeats only every two tiles), made seamless (near the edges it fades into a copy shifted by half the size, whose edges wrap around without a seam; `--band` pixels wide) and reduced to a palette without dithering. Writes out_prefix-.png and out_prefix--preview.png (3x3 copies scaled up 3x, to check the seams). Needs Pillow. """ import argparse from PIL import Image, ImageChops def seamless(img, band): n = img.width shifted = ImageChops.offset(img, n // 2, n // 2) mask = Image.new("L", (n, n)) for y in range(n): for x in range(n): # 0 at the edges (the shifted copy, continuous across the wrap), 255 inside d = min(x, n - 1 - x, y, n - 1 - y) mask.putpixel((x, y), int(255 * min(1.0, d / band))) return Image.composite(img, shifted, mask) def main(): ap = argparse.ArgumentParser() ap.add_argument("raw") ap.add_argument("out") ap.add_argument("--size", type=int, default=128) ap.add_argument("--colors", type=int, default=32) ap.add_argument("--band", type=int, default=14) a = ap.parse_args() img = Image.open(a.raw).convert("RGB") s = min(img.size) img = img.crop(((img.width - s) // 2, (img.height - s) // 2, (img.width + s) // 2, (img.height + s) // 2)) img = img.resize((a.size, a.size), Image.LANCZOS) img = seamless(img, a.band) img = img.quantize(colors=a.colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE).convert("RGB") img.save(f"{a.out}-{a.size}.png") prev = Image.new("RGB", (a.size * 3, a.size * 3)) for k in range(9): prev.paste(img, ((k % 3) * a.size, (k // 3) * a.size)) prev.resize((prev.width * 2, prev.height * 2), Image.NEAREST).save(f"{a.out}-{a.size}-preview.png") print(f"{a.out}-{a.size}.png") if __name__ == "__main__": main()