101 lines
4.1 KiB
Python
101 lines
4.1 KiB
Python
#!/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] [--variants N] [--bright F]
|
|
|
|
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-<size>.png and out_prefix-<size>-preview.png (3x3 copies scaled up 3x, to
|
|
check the seams).
|
|
|
|
--variants N: instead N single tiles of size/2 that fit together in any order: all share the
|
|
same seamless border (a quarter of the sheet made seamless on its own), their middles come from
|
|
different windows of the sheet. Writes out_prefix-v<tile>.png (the tiles side by side) and
|
|
out_prefix-v<tile>-preview.png (a floor of them laid out at random). Needs Pillow.
|
|
"""
|
|
import argparse
|
|
import random
|
|
|
|
from PIL import Image, ImageChops, ImageEnhance
|
|
|
|
|
|
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 window(img, x, y, n):
|
|
# an n x n window of a seamless picture, wrapping around
|
|
return ImageChops.offset(img, -x, -y).crop((0, 0, n, n))
|
|
|
|
|
|
def variants(sheet, a):
|
|
t = a.size // 2
|
|
base = seamless(sheet.crop((0, 0, t, t)), a.band)
|
|
band = max(4, t * 18 // 100)
|
|
mask = Image.new("L", (t, t))
|
|
for y in range(t):
|
|
for x in range(t):
|
|
d = min(x, t - 1 - x, y, t - 1 - y)
|
|
mask.putpixel((x, y), int(255 * max(0.0, min(1.0, (d - band * 0.4) / (band * 0.6)))))
|
|
tiles = [base]
|
|
for k in range(1, a.variants):
|
|
w = window(sheet, (k * t * 37 // 100 + t // 2 * (k % 2)) % a.size, (k * t * 61 // 100) % a.size, t)
|
|
tiles.append(Image.composite(w, base, mask))
|
|
strip = Image.new("RGB", (t * len(tiles), t))
|
|
for k, tile in enumerate(tiles):
|
|
strip.paste(tile, (k * t, 0))
|
|
strip = strip.quantize(colors=a.colors, method=Image.Quantize.MEDIANCUT, dither=Image.Dither.NONE).convert("RGB")
|
|
strip.save(f"{a.out}-v{t}.png")
|
|
rnd = random.Random(3)
|
|
prev = Image.new("RGB", (t * 6, t * 4))
|
|
for k in range(24):
|
|
v = rnd.randrange(len(tiles))
|
|
prev.paste(strip.crop((v * t, 0, (v + 1) * t, t)), ((k % 6) * t, (k // 6) * t))
|
|
prev.resize((prev.width * 2, prev.height * 2), Image.NEAREST).save(f"{a.out}-v{t}-preview.png")
|
|
print(f"{a.out}-v{t}.png: {len(tiles)} tiles")
|
|
|
|
|
|
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)
|
|
ap.add_argument("--variants", type=int, default=0)
|
|
ap.add_argument("--bright", type=float, default=1.0)
|
|
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)
|
|
if a.bright != 1.0:
|
|
img = ImageEnhance.Brightness(img).enhance(a.bright)
|
|
img = seamless(img, a.band)
|
|
if a.variants:
|
|
return variants(img, a)
|
|
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()
|