Hello there. I am an artist a little brainer towards technicalities. To create an Isometric map, my team generally paint each tile in photoshop but the end result looks too inorganic. I wonder there should be some way to paint a huge map in photoshop and slice into tiles for tile-set. Could you please help me with this?
Your question reminds me of this sad thread on the old Tiled mailing list, where somebody demonstrated that he had written a tool to do exactly that, but he never responded with the source code.
Right now Tiled can’t help you with this, but I could imagine shipping a little tool to do exactly that. Or to potentially even support using an imagine as a tileset directly with isometric shaped tiles, since that would be quite interesting from a texture memory and drawing overhead perspective. But that would be quite a big change.
In general, it’s just about making a mask image with sharp edges and using it to cut out the isometric shapes from your graphics. Though note that this is mostly interesting only for ground tiles, since stuff like trees and walls are easier to work with when they are not cut into diamond shapes.
I know this is an older thread, but I hope the bump is just a reminder that this feature is greatly sought after.
As a non-programmer, I have a tough time conceptualizing how to make an external tool to do this in Tiled. But really, and quite honestly, I am at the point where I would be willing to PayPal someone who could make the script necessary to make it work… It’s so sad that fellow pulled it off in 2012 but it never truly manifested…
Out of curiosity, supposing you did have a chance at developing such a script, what features would you envision to have in the tool, and how does the tool fit in any particular workflow?
For examples of what I mean
- I know that it is probably common-sense to expect the tool/script to accommodate different isometric tile resolutions, so the user must be able to input that.
- The tool may also feature a way to cut up any given images by a known number of iso tiles (assuming that the user has a properly sized image/canvas)
- The tool may feature wrapping so that the diamond iso tile that overflows from top/bottom/left/right edges are paired with its corresponding opposite half.
- Anti-aliasing / aliasing options
At its simplest these are the things that first come to mind. Are there any other ideas to add to that?
best,
lernie.
Yes, absolutely. All of these things.
Really, it would speed up workflow for me because I could sculpt an entire map in Blender, render in the appropriate aspect ratio, and break the render into tiles in the editor. Merely because of the design of my game, it would actually be easiest.
In addition, it would be great for the tool to discern the difference between the transparent and non-transparent sections of a PNG file, so it could rule out and discard unnecessary tiles. If you look at the link in Bjorn’s post, that fellow who made it in 2012 had the tool down perfectly. It just wasn’t submitted…
Edit: Like you said, specific resolution and/or pixel sizing options would be a must.
After almost an entire year, and two years after the creation of this thread, I’d like to bump it as a reminder that I still hope someone with the know-how can make a tool for this purpose…
Yeah, it is a matter of getting around to it… in the past year, I finished Tiled 1.1 which was mostly driven the Google Summer of Code students and now almost finished Tiled 1.2 which was mostly driven by priority requests from patrons (though the features are useful to many). And for Tiled 1.3 there are already often requested and very useful features planned as well (support for projects and scripting).
Of course, anybody is free to work on this!
There is also an issue about this regarding the same feature for hexagonal tiles.
Hello !,
Well, that’s a good discussion. I hope I’m doing well.
Sincerely sorry if this function exists and has been implemented.
I was searching for a script to create a sprite sheet from a drawing on Google.
I came across this thread. I kept searching online, but I couldn’t find anything.
So I spent about 8-10 hours with AI creating and refining a Python script.
After dozens, if not hundreds, of exchanges, I finally found the right recipe.
In my case, I’m using an offset isometric map, composed of 15x25 tiles of 256x128
(which made things quite complicated for me).
Behavior:
The script opens a window asking which image to process, and creates a folder containing all the tiles as well as a sprite sheet file outside with a transparent background. It only generates sprites for tiles with pixels inside. If the pixels present are artifacts, the script detects and deletes them. I applied a geometric rule: selection compression and content detection. If content = kept, if no content = moved to a “rejected” folder for verification.
I’ve done a lot of testing and it works perfectly. I hope this can be useful to someone like me who might be looking for this script.
Unfortunately, it’s not adaptable to all map types. Simply paste the code into gpt chat, along with your map .json file (just the dimensions, without any other information to keep the code light and short) and ask to adapt the script accordingly.
For those unfamiliar with Python, a 10-minute YouTube video will easily explain how to run a script. The only library you need besides Python for the script to work is: pip install pillow
cut-iso.py :
Summary
from PIL import Image, ImageDraw, ImageFilter, ImageChops
import os
import xml.etree.ElementTree as ET
from tkinter import Tk, filedialog
import math
import shutil
# --- Sélection de l'image ---
root = Tk()
root.withdraw()
img_path = filedialog.askopenfilename(
title="Sélectionne l'image à découper (Tiled staggered y/odd)",
filetypes=[("Images", "*.png;*.jpg;*.jpeg;*.bmp;*.tif;*.tiff")]
)
if not img_path:
print("❌ Aucune image sélectionnée. Abandon.")
exit()
image_dir = os.path.dirname(img_path)
image = Image.open(img_path).convert("RGBA")
img_w, img_h = image.size
# --- Lecture du TMX (si présent) ---
tmx_file = None
for f in os.listdir(image_dir):
if f.lower().endswith(".tmx"):
tmx_file = os.path.join(image_dir, f)
break
tile_width = 256
tile_height = 128
map_width = 15
map_height = 24
stagger_axis = "y"
stagger_index = "odd"
if tmx_file:
try:
root_xml = ET.parse(tmx_file).getroot()
tile_width = int(root_xml.get("tilewidth", tile_width))
tile_height = int(root_xml.get("tileheight", tile_height))
map_width = int(root_xml.get("width", map_width))
map_height = int(root_xml.get("height", map_height))
stagger_axis = root_xml.get("staggeraxis", "y")
stagger_index = root_xml.get("staggerindex", "odd")
print(f"📄 Paramètres lus depuis : {os.path.basename(tmx_file)}")
except Exception as e:
print(f"⚠️ Impossible de lire le TMX ({e}) — utilisation des valeurs par défaut")
# --- Création des dossiers de sortie ---
output_folder = os.path.join(image_dir, "tiles_iso")
rejected_folder = os.path.join(output_folder, "_rejected")
os.makedirs(output_folder, exist_ok=True)
os.makedirs(rejected_folder, exist_ok=True)
# --- Création du masque losange principal ---
def make_diamond_mask(w, h, shrink=0):
"""Crée un masque losange, éventuellement rétréci vers le centre."""
mask = Image.new("L", (w, h), 0)
draw = ImageDraw.Draw(mask)
points = [
(w / 2, shrink),
(w - shrink, h / 2),
(w / 2, h - shrink),
(shrink, h / 2)
]
draw.polygon(points, fill=255)
return mask
mask = make_diamond_mask(tile_width, tile_height)
mask = mask.filter(ImageFilter.MaxFilter(3))
mask = mask.point(lambda p: 255 if p > 128 else 0)
# --- Découpe des tuiles ---
tiles = []
odd_is_shifted = (stagger_index.lower() == "odd")
print("✂️ Découpe des tuiles...")
for y in range(map_height):
for x in range(map_width):
if stagger_axis.lower() == "y":
shift_x = tile_width / 2 if ((y % 2 == 1) == odd_is_shifted) else 0
px = x * tile_width + shift_x
py = y * (tile_height / 2.0)
else:
shift_y = tile_height / 2 if ((x % 2 == 1) == odd_is_shifted) else 0
px = x * (tile_width / 2.0)
py = y * tile_height + shift_y
left = round(px)
top = round(py)
right = min(round(px + tile_width), img_w)
bottom = min(round(py + tile_height), img_h)
crop = image.crop((left, top, right, bottom)).convert("RGBA")
# Application du masque losange
r, g, b, a = crop.split()
a = Image.composite(a, mask, mask)
tile = Image.merge("RGBA", (r, g, b, a)).convert("RGBA")
if tile.getbbox() is None:
continue
tile_filename = os.path.join(output_folder, f"tile_{y:02d}_{x:02d}.png")
tile.save(tile_filename)
tiles.append(tile_filename)
print(f"✅ {len(tiles)} tuiles isométriques non vides générées dans {output_folder}")
# --- Détection des tuiles indésirables ---
print("🔍 Vérification du contenu isométrique...")
def has_content_in_compressed_area(img, shrink_px=4):
"""Vérifie s’il existe au moins un pixel visible dans la zone losange compressée."""
alpha = img.getchannel("A")
mask_compressed = make_diamond_mask(img.width, img.height, shrink=shrink_px)
# Combine alpha (pixels visibles) avec le masque compressé
overlap = ImageChops.multiply(alpha, mask_compressed)
nonzero = sum(overlap.getdata()) / 255
return nonzero > 0
deleted_count = 0
for tile_path in tiles[:]:
img = Image.open(tile_path).convert("RGBA")
# Rejeter seulement si la zone compressée est totalement vide
if not has_content_in_compressed_area(img, shrink_px=4):
rejected_path = os.path.join(rejected_folder, os.path.basename(tile_path))
shutil.move(tile_path, rejected_path)
tiles.remove(tile_path)
deleted_count += 1
print(f"🧹 {deleted_count} tuiles déplacées vers _rejected/ (aucun contenu interne visible).")
# --- Génération du spritesheet propre ---
if tiles:
print("🧩 Génération du spritesheet final...")
tiles.sort()
tiles_per_row = min(10, len(tiles))
rows = math.ceil(len(tiles) / tiles_per_row)
sheet_w = tiles_per_row * tile_width
sheet_h = rows * tile_height
spritesheet = Image.new("RGBA", (sheet_w, sheet_h), (0, 0, 0, 0))
for i, tpath in enumerate(tiles):
t = Image.open(tpath).convert("RGBA")
tx = (i % tiles_per_row) * tile_width
ty = (i // tiles_per_row) * tile_height
spritesheet.paste(t, (tx, ty))
spritesheet_file = os.path.join(image_dir, "spritesheet_iso_clean.png")
spritesheet.save(spritesheet_file)
print(f"✅ Spritesheet propre généré : {spritesheet_file}")
else:
print("⚠️ Aucune tuile valide restante — spritesheet non généré.")