Hello!
For a game I'd like to create tiles for tileset. The task is simple: get a image with 3+ layers (colors, and heightmap), for each layer create 3 copy, rotate it 60, 180 ,270 degree and offset it 1*,2*,3* width.
After get the heightmap layer duplicate it, add noise and call the normal map filter. (there are some masking, etc). Get the color layers, add some nose. Flattering down each group of layers.
Because I'd like to use it many times, i created (or try to create) a plugin for it.
Looks like easy.
The problems: because its a python, you need to test is, for the typos too. so each iteration need to restart gimp load the image test the script.
There are no usable documentation. I already found the https://lazka.github.io/pgi-docs/#Gimp-3.0, helped a lot, but lack of examples. all examples for the gimp 2.x
For example, how can I do filter->generic->normal map?
There are some undocumented "features": If you rename a layer, in the gimp UI the name stay the old one (its not a big problem) but after you cant export it into .ora and gimp went some unstable state. If remove a layer from image cant add it again to it, because already had.
Now I have 300+ line of code for that "simple" task, about quarter are dead code ("maybe that work") and not working well:
- after I call <layer>.transform_rotate_simple(rotate_angle,True,0,0) its stay is "floating selection transform" state and somehow need to anchor it how?
- how can I proper call the filters->generic->normal map filter with preset or how set the values?
- how can I proper call the filters->noise->hsv filter with preset or how set the values?
- how can I invert selection?
- how can I proper generate greyscale noise between color rgb(123,123,123) and rgb (133,133,133), I tried the buffer.set(rect, "RGBA u8", bytes(pixels)) with pixels.extend([gray, gray, gray, 255]) it's worked, but not get the right color (I get lighter about rgb(180,180,180), now I use set_pixel but its slow for 64x64 images
- from where can I get some useful examples for gimp 3 python scripting?
IgnisVeneficus: Hi! You don't need to restart GIMP each time you edit your Python script. As long as it loads the first time successfully, you can edit the script and rerun it, and it should run the updated the version.
(The only exception is if you add new parameters to the procedure)
CmykStudent: Hi!
Thanx!
All is working. Now the script do what I wanted!
Just one little problem. After the script finished, I cant save/export the image, because all the menu items (save, save as, export, export as) as grayed out, and the hot keys don't work too. But I tried to close it, i get the "image is not saved" window. but i cant save with that too..
Thanx
(07-29-2025, 07:10 PM)IgnisVeneficus Wrote: CmykStudent: Hi!
Thanx!
All is working. Now the script do what I wanted!
Just one little problem. After the script finished, I cant save/export the image, because all the menu items (save, save as, export, export as) as grayed out, and the hot keys don't work too. But I tried to close it, i get the "image is not saved" window. but i cant save with that too..
Thanx
Hmm. Can you share the script? I can take a look and see if there's some issue.
import sys
import os
import gi
import random
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
from gi.repository import GLib
from gi.repository import Gio
procedure.set_documentation(_("Plug-in example in Python 3"),
_("Plug-in example in Python 3"),
name)
procedure.set_attribution("ignis", "ignis", "2025")
#create a layer, and fill up with grayscale noise between min and max value
def generate_noise(image,min,max,w,h):
layers = []
for i in range(4):
layer = Gimp.Layer.new(image, "Noise_" + str(i), w, h,
Gimp.ImageType.RGBA_IMAGE, 100.0, Gimp.LayerMode.NORMAL)
image.insert_layer(layer, None, -1)
color_format = "#{:02x}{:02x}{:02x}"
colors = []
for c in range(min,max+1):
colors.append(Gegl.Color.new(color_format.format(c,c,c)))
for y in range(h):
for x in range(w):
cp = random.randint(0,len(colors)-1)
#pixels.extend([gray, gray, gray, 255]) # R, G, B, A
layer.set_pixel(x,y,colors[cp])
#rename the layer: workaround: remove layer, copy layer, rename new layer, add to image the new layer
def rename_layer(image,layer,name):
if not layer:
return None
parent = layer.get_parent()
position = image.get_item_position(layer)
image.remove_layer(layer)
new_layer = layer.copy()
new_layer.set_name(name)
image.insert_layer(new_layer, parent, position)
return new_layer
#create rotated images and offset is
def generate_variants(image,layer,width):
if layer == None:
return [None]*4
variants = [layer]
for angle in range(1,4):
rotated = layer.copy()
rotated.set_name(layer.get_name()+"_"+str(angle))
image.insert_layer(rotated, None, -1)
rotate_angle = Gimp.RotationType.DEGREES90
if angle == 2:
rotate_angle =Gimp.RotationType.DEGREES180
if angle == 3:
rotate_angle =Gimp.RotationType.DEGREES270
rotated.transform_rotate_simple(rotate_angle,True,0,0)
floating=image.get_selected_layers()
# many times get an error, cant anchor not floating image
# if I leave it, one or two image not moved and/or not rotated
try:
Gimp.floating_sel_anchor(floating[0])
except Exception as e:
pass
rotated.set_offsets(angle * width, 0)
variants.append(rotated)
return variants
# get the mask layer, select the black color, and fill the selection with neutral gray on the noise layer
def mask_noise(componed_layers,image):
Gimp.context_set_background(Gegl.Color.new("#808080"))
for i in range(4):
mask = componed_layers["Mask"][i]
if mask!=None:
image.select_color(Gimp.ChannelOps.REPLACE, mask, Gegl.Color.new("#000000"))
noise_layer = componed_layers["Noise"][i]
image.active_layer = noise_layer
noise_layer.edit_fill(Gimp.FillType.BACKGROUND)
# to be sure noting is selected
selection = image.get_selection()
selection.none(image)
# get the noise layer, the normal layer, and the noise addon, and merged together
def merge_noise(componed_layers,image):
#merge noise es tarsai:
for i in range(4):
noise = componed_layers["Noise"][i]
normal = componed_layers["Normal"][i]
addon = componed_layers["Addon"][i]
noise.set_visible(True)
normal.set_visible(True)
image.reorder_item(normal,None,-1)
image.reorder_item(noise, None,-1)
name = normal.get_name()
Gimp.message(normal.get_name())
if addon!=None:
addon.set_visible(True)
image.reorder_item(addon, None,-1)
image.active_layer = normal
normal = image.merge_visible_layers(Gimp.MergeType.EXPAND_AS_NECESSARY)
#need to rename the last layer
Gimp.message(normal.get_name())
normal=rename_layer(image,normal,name)
componed_layers["Normal"][i]=normal
normal.set_visible(False)
# generate noise for the Color image
# get the mask, select the black, invert the selection, and generate the noise into the selection on the color layer
def color_noise(componed_layers,image):
for i in range(4):
color = componed_layers["Color"][i]
mask = componed_layers["ColorMask"][i]
color.set_visible(True)
image.reorder_item(color,None,-1)
apply = color
if mask!=None:
mask.set_visible(True)
image.reorder_item(mask, None,-1)
image.select_color(Gimp.ChannelOps.REPLACE, mask, Gegl.Color.new("#000000"))
selection = image.get_selection()
selection.invert(image)
apply = selection
image.active_layer = color
apply_noise(apply)
selection = image.get_selection()
if selection:
selection.none(image)
color.set_visible(False)
# main function
def tileset_variants(image, drawable):
Gimp.message("variant running")
# get the file name to the export
orig_path = image.get_file().get_path()
basename = os.path.basename(orig_path)
dirname = os.path.dirname(orig_path)
new_name = os.path.splitext(basename)[0] + ".ora"
output_path = os.path.join(dirname, new_name)
Gimp.message("before")
image.undo_group_start()
#set up dedicated layers
color = None
heightmap = None
mask = None
normal = None
addon = None
colormask = None
#Extract layers
for layer in image.get_layers():
layer.set_visible(False)
# Color: visible texture
if layer.get_name() == "Color":
color = layer
# Heightmap
if layer.get_name() == "Heightmap":
heightmap = layer
# Mask, where dont need noise for heightmap
if layer.get_name() == "Noisemask":
mask = layer
# layer added to the heightmap aka details
if layer.get_name() == "Heightaddon":
addon = layer
# color mask, where deont need noise for color layer
if layer.get_name() == "Colormask":
colormask = layer
# duplicate heightmap, rename to normal
normal = heightmap.copy()
normal.set_name("Normal")
image.insert_layer(normal, None, -1)
#collect the layers
all_layers = {"Color":color, "Heightmap":heightmap, "Normal":normal,"Mask":mask,"Addon":addon,"ColorMask":colormask}
# rotate and shift, create dict for layers
componed_layers={}
for name,layer in all_layers.items():
variants = generate_variants(image,layer,width)
componed_layers[name]=variants
#create heightmap for normal (apply noise, mask, details onto the copied heightmap)
merge_noise(componed_layers,image)
#apply normal map filter
for i in range(4):
normal = componed_layers["Normal"][i]
apply_normalMap(normal)
#apply color noise
color_noise(componed_layers,image)
#merge down each group
Gimp.message("flattening")
for name,layer in componed_layers.items():
Gimp.message("group: "+name)
needMerge = False
for l in layer:
if l:
Gimp.message("layer: "+l.get_name())
l.set_visible(True)
image.active_layer = l
needMerge = True
if needMerge:
merged = image.merge_visible_layers(Gimp.MergeType.EXPAND_AS_NECESSARY)
if merged:
Gimp.message("rename: "+merged.get_name())
merged=rename_layer(image,merged,name)
merged.set_visible(False)
image.active_layer = merged
#try to export as openraster
Gimp.file_save(Gimp.RunMode.INTERACTIVE,image, Gio.File.new_for_path(output_path),None)
IgnisVeneficus: Hi again! I haven't been able to replicate the problem so far - it lets me export or save the final result. I did notice one thing - by default, GIMP does not create floating selections anymore when you copy and paste. Instead, it creates a new layer. That's why Gimp.floating_sel_anchor () throws errors. You'll either need to paste as a floating layer or merge the layer down instead.
Hello!
About the floating selection.
If I comment out the whole anchore thing, I noticed, during the duplicate-rotate-move there is a floating layer, and the first layer, first target is missing/or not moved/rotated
(see attachement)