1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 17:19:50 +00:00

Add Palettize and Pixelize effects

Pixelize makes the image pixelated, and Palettize maps the color of the input to the nearest color in the selected palette. Useful for limiting color in pixel art and for artistic effects.
This commit is contained in:
Emmanouil Papadeas 2024-04-10 01:20:28 +03:00
parent dbfd4d8412
commit 1c9c8bf4e3
13 changed files with 235 additions and 6 deletions

View file

@ -978,6 +978,14 @@ msgstr ""
msgid "Steps:"
msgstr ""
#. An image effect. It maps the color of the input to the nearest color in the selected palette. Useful for limiting color in pixel art and for artistic effects.
msgid "Palettize"
msgstr ""
#. An image effect. It makes the input image pixelated.
msgid "Pixelize"
msgstr ""
#. An image effect. For more details about what it does, you can refer to GIMP's documentation https://docs.gimp.org/2.8/en/gimp-tool-posterize.html
msgid "Posterize"
msgstr ""

View file

@ -845,6 +845,14 @@ project_properties={
"deadzone": 0.5,
"events": []
}
palettize={
"deadzone": 0.5,
"events": []
}
pixelize={
"deadzone": 0.5,
"events": []
}
[input_devices]

View file

@ -59,6 +59,8 @@ enum EffectsMenu {
INVERT_COLORS,
DESATURATION,
HSV,
PALETTIZE,
PIXELIZE,
POSTERIZE,
GRADIENT,
GRADIENT_MAP,
@ -739,6 +741,8 @@ func _initialize_keychain() -> void:
"adjust_hsv": Keychain.InputAction.new("", "Effects menu", true),
"gradient": Keychain.InputAction.new("", "Effects menu", true),
"gradient_map": Keychain.InputAction.new("", "Effects menu", true),
&"palettize": Keychain.InputAction.new("", "Effects menu", true),
&"pixelize": Keychain.InputAction.new("", "Effects menu", true),
"posterize": Keychain.InputAction.new("", "Effects menu", true),
"mirror_view": Keychain.InputAction.new("", "View menu", true),
"show_grid": Keychain.InputAction.new("", "View menu", true),
@ -1144,14 +1148,16 @@ func create_ui_for_shader_uniforms(
hbox.add_child(label)
hbox.add_child(slider)
parent_node.add_child(hbox)
elif u_type == "vec2":
elif u_type == "vec2" or u_type == "ivec2" or u_type == "uvec2":
var label := Label.new()
label.text = humanized_u_name
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var vector2 := _vec2str_to_vector2(u_value)
var slider := VALUE_SLIDER_V2_TSCN.instantiate() as ValueSliderV2
slider.show_ratio = true
slider.allow_greater = true
slider.allow_lesser = true
if u_type != "uvec2":
slider.allow_lesser = true
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
slider.value = vector2
if params.has(u_name):
@ -1186,6 +1192,17 @@ func create_ui_for_shader_uniforms(
elif u_type == "sampler2D":
if u_name == "selection":
continue
if u_name == "palette_texture":
var palette := Palettes.current_palette
var palette_texture := ImageTexture.create_from_image(palette.convert_to_image())
value_changed.call(palette_texture, u_name)
Palettes.palette_selected.connect(
func(_name): _shader_change_palette(value_changed, u_name)
)
palette.data_changed.connect(
func(): _shader_update_palette_texture(palette, value_changed, u_name)
)
continue
var label := Label.new()
label.text = humanized_u_name
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
@ -1241,6 +1258,8 @@ func create_ui_for_shader_uniforms(
func _vec2str_to_vector2(vec2: String) -> Vector2:
vec2 = vec2.replace("uvec2", "vec2")
vec2 = vec2.replace("ivec2", "vec2")
vec2 = vec2.replace("vec2(", "")
vec2 = vec2.replace(")", "")
var vec_values := vec2.split(",")
@ -1272,3 +1291,18 @@ func _vec4str_to_color(vec4: String) -> Color:
alpha = float(rgba_values[3])
var color := Color(red, green, blue, alpha)
return color
func _shader_change_palette(value_changed: Callable, parameter_name: String) -> void:
var palette := Palettes.current_palette
_shader_update_palette_texture(palette, value_changed, parameter_name)
#if not palette.data_changed.is_connected(_shader_update_palette_texture):
palette.data_changed.connect(
func(): _shader_update_palette_texture(palette, value_changed, parameter_name)
)
func _shader_update_palette_texture(
palette: Palette, value_changed: Callable, parameter_name: String
) -> void:
value_changed.call(ImageTexture.create_from_image(palette.convert_to_image()), parameter_name)

View file

@ -1,6 +1,8 @@
class_name Palette
extends RefCounted
signal data_changed
const DEFAULT_WIDTH := 8
const DEFAULT_HEIGHT := 8
@ -172,6 +174,7 @@ func add_color(new_color: Color, start_index := 0) -> void:
if not colors.has(i):
colors[i] = PaletteColor.new(new_color, i)
break
data_changed.emit()
## Returns color at index or null if no color exists
@ -186,11 +189,13 @@ func get_color(index: int):
func set_color(index: int, new_color: Color) -> void:
if colors.has(index):
colors[index].color = new_color
data_changed.emit()
## Removes a color at the specified index
func remove_color(index: int) -> void:
colors.erase(index)
data_changed.emit()
## Inserts a color to the specified index
@ -200,12 +205,13 @@ func insert_color(index: int, new_color: Color) -> void:
# If insert happens on non empty swatch recursively move the original color
# and every other color to its right one swatch to right
if colors[index] != null:
move_right(index)
_move_right(index)
colors[index] = c
data_changed.emit()
## Recursive function that moves every color to right until one of them is moved to empty swatch
func move_right(index: int) -> void:
func _move_right(index: int) -> void:
# Moving colors to right would overflow the size of the palette
# so increase its height automatically
if index + 1 == colors_max:
@ -214,7 +220,7 @@ func move_right(index: int) -> void:
# If swatch to right to this color is not empty move that color right too
if colors[index + 1] != null:
move_right(index + 1)
_move_right(index + 1)
colors[index + 1] = colors[index]
@ -237,6 +243,7 @@ func swap_colors(from_index: int, to_index: int) -> void:
colors[to_index].index = to_index
colors[from_index] = to_color
colors[from_index].index = from_index
data_changed.emit()
## Copies color
@ -245,6 +252,7 @@ func copy_colors(from_index: int, to_index: int) -> void:
if colors[from_index] != null:
colors[to_index] = colors[from_index].duplicate()
colors[to_index].index = to_index
data_changed.emit()
func reverse_colors() -> void:
@ -254,6 +262,7 @@ func reverse_colors() -> void:
for i in reversed_colors.size():
reversed_colors[i].index = i
colors[i] = reversed_colors[i]
data_changed.emit()
func sort(option: Palettes.SortOptions) -> void:
@ -279,6 +288,7 @@ func sort(option: Palettes.SortOptions) -> void:
for i in sorted_colors.size():
sorted_colors[i].index = i
colors[i] = sorted_colors[i]
data_changed.emit()
## True if all swatches are occupied

View file

@ -0,0 +1,33 @@
// Maps the color of the input to the nearest color in the selected palette.
// Similar to Krita's Palettize filter
shader_type canvas_item;
uniform sampler2D palette_texture : filter_nearest;
uniform sampler2D selection;
vec4 swap_color(vec4 color) {
if (color.a <= 0.01) {
return color;
}
int color_index = 0;
int n_of_colors = textureSize(palette_texture, 0).x;
float smaller_distance = distance(color, texture(palette_texture, vec2(0.0)));
for (int i = 0; i <= n_of_colors; i++) {
vec2 uv = vec2(float(i) / float(n_of_colors), 0.0);
vec4 palette_color = texture(palette_texture, uv);
float dist = distance(color, palette_color);
if (dist < smaller_distance) {
smaller_distance = dist;
color_index = i;
}
}
return texture(palette_texture, vec2(float(color_index) / float(n_of_colors), 0.0));
}
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec4 selection_color = texture(selection, UV);
vec4 color = swap_color(original_color);
COLOR = mix(original_color.rgba, color, selection_color.a);
}

View file

@ -0,0 +1,25 @@
/*
Shader from Godot Shaders - the free shader library.
https://godotshaders.com/shader/pixelate-2/
This shader is under MIT license
*/
shader_type canvas_item;
uniform uvec2 pixel_size = uvec2(4);
uniform sampler2D selection;
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec4 selection_color = texture(selection, UV);
ivec2 size = textureSize(TEXTURE, 0);
int xRes = size.x;
int yRes = size.y;
float xFactor = float(xRes) / float(pixel_size.x);
float yFactor = float(yRes) / float(pixel_size.y);
float grid_uv_x = round(UV.x * xFactor) / xFactor;
float grid_uv_y = round(UV.y * yFactor) / yFactor;
vec4 pixelated_color = texture(TEXTURE, vec2(grid_uv_x, grid_uv_y));
COLOR = mix(original_color.rgba, pixelated_color, selection_color.a);
}

View file

@ -0,0 +1,29 @@
extends ImageEffect
var shader := preload("res://src/Shaders/Effects/Palettize.gdshader")
func _ready() -> void:
super._ready()
var sm := ShaderMaterial.new()
sm.shader = shader
preview.set_material(sm)
func commit_action(cel: Image, project := Global.current_project) -> void:
var selection_tex: ImageTexture
if selection_checkbox.button_pressed and project.has_selection:
selection_tex = ImageTexture.create_from_image(project.selection_map)
if not is_instance_valid(Palettes.current_palette):
return
var palette_image := Palettes.current_palette.convert_to_image()
var palette_texture := ImageTexture.create_from_image(palette_image)
var params := {"palette_texture": palette_texture, "selection": selection_tex}
if !has_been_confirmed:
for param in params:
preview.material.set_shader_parameter(param, params[param])
else:
var gen := ShaderImageEffect.new()
gen.generate_image(cel, shader, params, project.size)

View file

@ -0,0 +1,11 @@
[gd_scene load_steps=3 format=3 uid="uid://d4gbo50bjenut"]
[ext_resource type="PackedScene" uid="uid://bybqhhayl5ay5" path="res://src/UI/Dialogs/ImageEffects/ImageEffectParent.tscn" id="1_cux3a"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/PalettizeDialog.gd" id="2_4517g"]
[node name="PalettizeDialog" instance=ExtResource("1_cux3a")]
title = "Palettize"
script = ExtResource("2_4517g")
[node name="ShowAnimate" parent="VBoxContainer" index="0"]
visible = false

View file

@ -0,0 +1,30 @@
extends ImageEffect
var shader := preload("res://src/Shaders/Effects/Pixelize.gdshader")
var pixel_size := Vector2i.ONE
func _ready() -> void:
super._ready()
var sm := ShaderMaterial.new()
sm.shader = shader
preview.set_material(sm)
func commit_action(cel: Image, project := Global.current_project) -> void:
var selection_tex: ImageTexture
if selection_checkbox.button_pressed and project.has_selection:
selection_tex = ImageTexture.create_from_image(project.selection_map)
var params := {"pixel_size": pixel_size, "selection": selection_tex}
if !has_been_confirmed:
for param in params:
preview.material.set_shader_parameter(param, params[param])
else:
var gen := ShaderImageEffect.new()
gen.generate_image(cel, shader, params, project.size)
func _on_pixel_size_value_changed(value: Vector2) -> void:
pixel_size = value
update_preview()

View file

@ -0,0 +1,31 @@
[gd_scene load_steps=4 format=3 uid="uid://ts831nyvn6y7"]
[ext_resource type="PackedScene" uid="uid://bybqhhayl5ay5" path="res://src/UI/Dialogs/ImageEffects/ImageEffectParent.tscn" id="1_eiotn"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/PixelizeDialog.gd" id="2_x5pd6"]
[ext_resource type="PackedScene" path="res://src/UI/Nodes/ValueSliderV2.tscn" id="3_s7ey1"]
[node name="PixelizeDialog" instance=ExtResource("1_eiotn")]
title = "Pixelize"
script = ExtResource("2_x5pd6")
[node name="ShowAnimate" parent="VBoxContainer" index="0"]
visible = false
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer" index="2"]
layout_mode = 2
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer" index="0"]
layout_mode = 2
size_flags_horizontal = 3
text = "Pixel size:"
[node name="PixelSize" parent="VBoxContainer/HBoxContainer" index="1" instance=ExtResource("3_s7ey1")]
layout_mode = 2
size_flags_horizontal = 3
value = Vector2(1, 1)
min_value = Vector2(1, 1)
max_value = Vector2(255, 255)
allow_greater = true
show_ratio = true
[connection signal="value_changed" from="VBoxContainer/HBoxContainer/PixelSize" to="." method="_on_pixel_size_value_changed"]

View file

@ -4,7 +4,7 @@ extends HBoxContainer
## A class that combines two ValueSlider nodes, for easy usage with Vector2 values.
## Also supports aspect ratio locking.
signal value_changed(value: float)
signal value_changed(value: Vector2)
signal ratio_toggled(button_pressed: bool)
@export var editable := true:

View file

@ -13,6 +13,8 @@ var effects: Array[LayerEffect] = [
LayerEffect.new(
"Adjust Hue/Saturation/Value", preload("res://src/Shaders/Effects/HSV.gdshader")
),
LayerEffect.new("Palettize", preload("res://src/Shaders/Effects/Palettize.gdshader")),
LayerEffect.new("Pixelize", preload("res://src/Shaders/Effects/Pixelize.gdshader")),
LayerEffect.new("Posterize", preload("res://src/Shaders/Effects/Posterize.gdshader")),
LayerEffect.new("Gradient Map", preload("res://src/Shaders/Effects/GradientMap.gdshader")),
]

View file

@ -29,6 +29,8 @@ var drop_shadow_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/DropShad
var hsv_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/HSVDialog.tscn")
var gradient_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientDialog.tscn")
var gradient_map_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn")
var palettize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn")
var pixelize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn")
var posterize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/Posterize.tscn")
var shader_effect_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn")
var manage_layouts_dialog := Dialog.new("res://src/UI/Dialogs/ManageLayouts.tscn")
@ -364,6 +366,8 @@ func _setup_effects_menu() -> void:
"Invert Colors": "invert_colors",
"Desaturation": "desaturation",
"Adjust Hue/Saturation/Value": "adjust_hsv",
"Palettize": "palettize",
"Pixelize": "pixelize",
"Posterize": "posterize",
"Gradient": "gradient",
"Gradient Map": "gradient_map",
@ -771,6 +775,10 @@ func effects_menu_id_pressed(id: int) -> void:
gradient_dialog.popup()
Global.EffectsMenu.GRADIENT_MAP:
gradient_map_dialog.popup()
Global.EffectsMenu.PALETTIZE:
palettize_dialog.popup()
Global.EffectsMenu.PIXELIZE:
pixelize_dialog.popup()
Global.EffectsMenu.POSTERIZE:
posterize_dialog.popup()
#Global.EffectsMenu.SHADER: