From 1c9c8bf4e3c38e1e9dfae521d256a35754a610c1 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Wed, 10 Apr 2024 01:20:28 +0300 Subject: [PATCH] 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. --- Translations/Translations.pot | 8 ++++ project.godot | 8 ++++ src/Autoload/Global.gd | 38 ++++++++++++++++++- src/Palette/Palette.gd | 16 ++++++-- src/Shaders/Effects/Palettize.gdshader | 33 ++++++++++++++++ src/Shaders/Effects/Pixelize.gdshader | 25 ++++++++++++ .../Dialogs/ImageEffects/PalettizeDialog.gd | 29 ++++++++++++++ .../Dialogs/ImageEffects/PalettizeDialog.tscn | 11 ++++++ src/UI/Dialogs/ImageEffects/PixelizeDialog.gd | 30 +++++++++++++++ .../Dialogs/ImageEffects/PixelizeDialog.tscn | 31 +++++++++++++++ src/UI/Nodes/ValueSliderV2.gd | 2 +- .../LayerEffects/LayerEffectsSettings.gd | 2 + src/UI/TopMenuContainer/TopMenuContainer.gd | 8 ++++ 13 files changed, 235 insertions(+), 6 deletions(-) create mode 100644 src/Shaders/Effects/Palettize.gdshader create mode 100644 src/Shaders/Effects/Pixelize.gdshader create mode 100644 src/UI/Dialogs/ImageEffects/PalettizeDialog.gd create mode 100644 src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn create mode 100644 src/UI/Dialogs/ImageEffects/PixelizeDialog.gd create mode 100644 src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn diff --git a/Translations/Translations.pot b/Translations/Translations.pot index ef3eed7ab..9d8ed8aca 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -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 "" diff --git a/project.godot b/project.godot index 4f180f578..18bf4df63 100644 --- a/project.godot +++ b/project.godot @@ -845,6 +845,14 @@ project_properties={ "deadzone": 0.5, "events": [] } +palettize={ +"deadzone": 0.5, +"events": [] +} +pixelize={ +"deadzone": 0.5, +"events": [] +} [input_devices] diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 65599978b..ebff80274 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -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) diff --git a/src/Palette/Palette.gd b/src/Palette/Palette.gd index c4bf97df0..6f455e6a6 100644 --- a/src/Palette/Palette.gd +++ b/src/Palette/Palette.gd @@ -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 diff --git a/src/Shaders/Effects/Palettize.gdshader b/src/Shaders/Effects/Palettize.gdshader new file mode 100644 index 000000000..b812aa945 --- /dev/null +++ b/src/Shaders/Effects/Palettize.gdshader @@ -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); +} + diff --git a/src/Shaders/Effects/Pixelize.gdshader b/src/Shaders/Effects/Pixelize.gdshader new file mode 100644 index 000000000..02e9dfbc2 --- /dev/null +++ b/src/Shaders/Effects/Pixelize.gdshader @@ -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); +} diff --git a/src/UI/Dialogs/ImageEffects/PalettizeDialog.gd b/src/UI/Dialogs/ImageEffects/PalettizeDialog.gd new file mode 100644 index 000000000..811cbd1a6 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/PalettizeDialog.gd @@ -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) diff --git a/src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn b/src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn new file mode 100644 index 000000000..c6501f653 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn @@ -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 diff --git a/src/UI/Dialogs/ImageEffects/PixelizeDialog.gd b/src/UI/Dialogs/ImageEffects/PixelizeDialog.gd new file mode 100644 index 000000000..3fd1ce7ce --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/PixelizeDialog.gd @@ -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() diff --git a/src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn b/src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn new file mode 100644 index 000000000..438ca9715 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/PixelizeDialog.tscn @@ -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"] diff --git a/src/UI/Nodes/ValueSliderV2.gd b/src/UI/Nodes/ValueSliderV2.gd index 285acbf11..25dcfba0f 100644 --- a/src/UI/Nodes/ValueSliderV2.gd +++ b/src/UI/Nodes/ValueSliderV2.gd @@ -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: diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 08bb97793..eed168ec8 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -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")), ] diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 4f5a0f0c4..02cd7021b 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -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: