From ec2dcae8f7600077432f75739ea1ec187262cfa8 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas Date: Thu, 20 Apr 2023 16:08:06 +0300 Subject: [PATCH] Add a Posterize image effect, with optional dithering Seems to produce the same result as GIMP's Posterize color filter. Should be useful for reducing the colors of an image, and it could work together with 3D lighting. Thanks to https://godotshaders.com/shader/color-reduction-and-dither/ for the shader. --- Translations/Translations.pot | 12 +-- project.godot | 12 ++- src/Autoload/Global.gd | 3 + src/Shaders/Posterize.gdshader | 27 ++++++ src/UI/Dialogs/ImageEffects/ImageEffects.tscn | 5 +- src/UI/Dialogs/ImageEffects/Posterize.gd | 43 ++++++++++ src/UI/Dialogs/ImageEffects/Posterize.tscn | 83 +++++++++++++++++++ src/UI/TopMenuContainer/TopMenuContainer.gd | 4 + 8 files changed, 178 insertions(+), 11 deletions(-) create mode 100644 src/Shaders/Posterize.gdshader create mode 100644 src/UI/Dialogs/ImageEffects/Posterize.gd create mode 100644 src/UI/Dialogs/ImageEffects/Posterize.tscn diff --git a/Translations/Translations.pot b/Translations/Translations.pot index fcca899e0..11133defc 100644 --- a/Translations/Translations.pot +++ b/Translations/Translations.pot @@ -855,16 +855,16 @@ msgstr "" msgid "Steps:" msgstr "" -msgid "Top to Bottom" +#. 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 "" -msgid "Bottom to Top" +#. An option for the posterize 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 levels:" msgstr "" -msgid "Left to Right" -msgstr "" - -msgid "Right to Left" +#. An option for the posterize image effect. +msgid "Dither intensity:" msgstr "" msgid "View Splash Screen" diff --git a/project.godot b/project.godot index c363c4427..0abe4a7a7 100644 --- a/project.godot +++ b/project.godot @@ -988,6 +988,14 @@ gradient={ "deadzone": 0.5, "events": [ ] } +gradient_map={ +"deadzone": 0.5, +"events": [ ] +} +posterize={ +"deadzone": 0.5, +"events": [ ] +} view_splash_screen={ "deadzone": 0.5, "events": [ ] @@ -1008,10 +1016,6 @@ about_pixelorama={ "deadzone": 0.5, "events": [ ] } -gradient_map={ -"deadzone": 0.5, -"events": [ ] -} left_paint_selection_tool={ "deadzone": 0.5, "events": [ Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"alt":false,"shift":false,"control":false,"meta":false,"command":false,"pressed":false,"scancode":73,"physical_scancode":0,"unicode":0,"echo":false,"script":null) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index f91739143..70a1fdf0f 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -37,6 +37,7 @@ enum ImageMenu { HSV, GRADIENT, GRADIENT_MAP, + POSTERIZE, SHADER } enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT } @@ -310,6 +311,8 @@ func _initialize_keychain() -> void: Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.GRADIENT), "gradient_map": Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.GRADIENT_MAP), + "posterize": + Keychain.MenuInputAction.new("", "Image menu", true, "ImageMenu", ImageMenu.POSTERIZE), "mirror_view": Keychain.MenuInputAction.new("", "View menu", true, "ViewMenu", ViewMenu.MIRROR_VIEW), "show_grid": diff --git a/src/Shaders/Posterize.gdshader b/src/Shaders/Posterize.gdshader new file mode 100644 index 000000000..9226d4fbe --- /dev/null +++ b/src/Shaders/Posterize.gdshader @@ -0,0 +1,27 @@ +// https://godotshaders.com/shader/color-reduction-and-dither/ +shader_type canvas_item; + +uniform sampler2D selection; +uniform float colors : hint_range(1.0, 255.0) = 2.0; +uniform float dither : hint_range(0.0, 0.5) = 0.0; + +void fragment() +{ + vec4 color = texture(TEXTURE, UV); + vec4 selection_color = texture(selection, UV); + + float a = floor(mod(UV.x / TEXTURE_PIXEL_SIZE.x, 2.0)); + float b = floor(mod(UV.y / TEXTURE_PIXEL_SIZE.y, 2.0)); + float c = mod(a + b, 2.0); + vec4 col; + col.r = (round(color.r * colors + dither) / colors) * c; + col.g = (round(color.g * colors + dither) / colors) * c; + col.b = (round(color.b * colors + dither) / colors) * c; + c = 1.0 - c; + col.r += (round(color.r * colors - dither) / colors) * c; + col.g += (round(color.g * colors - dither) / colors) * c; + col.b += (round(color.b * colors - dither) / colors) * c; + col.a = color.a; + vec4 output = mix(color.rgba, col, selection_color.a); + COLOR = output; +} diff --git a/src/UI/Dialogs/ImageEffects/ImageEffects.tscn b/src/UI/Dialogs/ImageEffects/ImageEffects.tscn index 45da9b43a..a9715a971 100644 --- a/src/UI/Dialogs/ImageEffects/ImageEffects.tscn +++ b/src/UI/Dialogs/ImageEffects/ImageEffects.tscn @@ -1,10 +1,11 @@ -[gd_scene load_steps=13 format=2] +[gd_scene load_steps=14 format=2] [ext_resource path="res://src/UI/Dialogs/ImageEffects/FlipImageDialog.tscn" type="PackedScene" id=1] [ext_resource path="res://src/UI/Dialogs/ImageEffects/InvertColorsDialog.tscn" type="PackedScene" id=2] [ext_resource path="res://src/UI/Dialogs/ImageEffects/DesaturateDialog.tscn" type="PackedScene" id=3] [ext_resource path="res://src/UI/Dialogs/ImageEffects/DropShadowDialog.tscn" type="PackedScene" id=4] [ext_resource path="res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn" type="PackedScene" id=5] +[ext_resource path="res://src/UI/Dialogs/ImageEffects/Posterize.tscn" type="PackedScene" id=6] [ext_resource path="res://src/UI/Dialogs/ImageEffects/ResizeCanvas.tscn" type="PackedScene" id=8] [ext_resource path="res://src/UI/Dialogs/ImageEffects/RotateImage.tscn" type="PackedScene" id=9] [ext_resource path="res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn" type="PackedScene" id=10] @@ -44,4 +45,6 @@ margin_bottom = 214.0 [node name="GradientMapDialog" parent="." instance=ExtResource( 5 )] +[node name="Posterize" parent="." instance=ExtResource( 6 )] + [node name="ShaderEffect" parent="." instance=ExtResource( 10 )] diff --git a/src/UI/Dialogs/ImageEffects/Posterize.gd b/src/UI/Dialogs/ImageEffects/Posterize.gd new file mode 100644 index 000000000..373dc87c1 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/Posterize.gd @@ -0,0 +1,43 @@ +extends ImageEffect + +var shader: Shader = preload("res://src/Shaders/Posterize.gdshader") +var levels := 2.0 +var dither := 0.0 + + +func _ready() -> void: + var sm := ShaderMaterial.new() + sm.shader = shader + preview.set_material(sm) + + +func set_nodes() -> void: + preview = $VBoxContainer/AspectRatioContainer/Preview + selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox + affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton + + +func commit_action(cel: Image, project: Project = Global.current_project) -> void: + var selection_tex := ImageTexture.new() + if selection_checkbox.pressed and project.has_selection: + selection_tex.create_from_image(project.selection_map, 0) + + var params := {"colors": levels, "dither": dither, "selection": selection_tex} + + if !confirmed: + for param in params: + preview.material.set_shader_param(param, params[param]) + else: + var gen := ShaderImageEffect.new() + gen.generate_image(cel, shader, params, project.size) + yield(gen, "done") + + +func _on_LevelsSlider_value_changed(value: float) -> void: + levels = value - 1.0 + update_preview() + + +func _on_DitherSlider_value_changed(value: float) -> void: + dither = value + update_preview() diff --git a/src/UI/Dialogs/ImageEffects/Posterize.tscn b/src/UI/Dialogs/ImageEffects/Posterize.tscn new file mode 100644 index 000000000..bcf935b88 --- /dev/null +++ b/src/UI/Dialogs/ImageEffects/Posterize.tscn @@ -0,0 +1,83 @@ +[gd_scene load_steps=4 format=2] + +[ext_resource path="res://src/UI/Dialogs/ImageEffects/ImageEffectParent.tscn" type="PackedScene" id=1] +[ext_resource path="res://src/UI/Nodes/ValueSlider.gd" type="Script" id=2] +[ext_resource path="res://src/UI/Dialogs/ImageEffects/Posterize.gd" type="Script" id=3] + +[node name="Posterize" instance=ExtResource( 1 )] +window_title = "Posterize" +script = ExtResource( 3 ) + +[node name="VBoxContainer" parent="." index="3"] +margin_bottom = 292.0 + +[node name="AspectRatioContainer" parent="VBoxContainer" index="0"] +margin_right = 278.0 + +[node name="Preview" parent="VBoxContainer/AspectRatioContainer" index="0"] +margin_left = 39.0 +margin_right = 239.0 + +[node name="LevelsSlider" type="TextureProgress" parent="VBoxContainer" index="1"] +margin_top = 204.0 +margin_right = 278.0 +margin_bottom = 228.0 +rect_min_size = Vector2( 0, 24 ) +mouse_default_cursor_shape = 2 +theme_type_variation = "ValueSlider" +min_value = 2.0 +max_value = 256.0 +step = 0.01 +value = 3.0 +nine_patch_stretch = true +stretch_margin_left = 3 +stretch_margin_top = 3 +stretch_margin_right = 3 +stretch_margin_bottom = 3 +script = ExtResource( 2 ) +prefix = "Posterize levels:" +snap_by_default = true + +[node name="DitherSlider" type="TextureProgress" parent="VBoxContainer" index="2"] +margin_top = 232.0 +margin_right = 278.0 +margin_bottom = 256.0 +rect_min_size = Vector2( 0, 24 ) +mouse_default_cursor_shape = 2 +theme_type_variation = "ValueSlider" +max_value = 0.5 +step = 0.01 +nine_patch_stretch = true +stretch_margin_left = 3 +stretch_margin_top = 3 +stretch_margin_right = 3 +stretch_margin_bottom = 3 +script = ExtResource( 2 ) +prefix = "Dither intensity:" +snap_step = 0.1 + +[node name="OptionsContainer" parent="VBoxContainer" index="3"] +margin_top = 260.0 +margin_right = 278.0 +margin_bottom = 284.0 + +[node name="AffectOptionButton" parent="VBoxContainer/OptionsContainer" index="1"] +margin_right = 278.0 +items = [ "Selected cels", null, false, 0, null, "Current frame", null, false, 1, null, "All frames", null, false, 2, null, "All projects", null, false, 3, null ] + +[node name="AnimationOptions" parent="VBoxContainer" index="4"] +visible = false +margin_right = 278.0 + +[node name="PanelContainer" parent="VBoxContainer/AnimationOptions" index="1"] +margin_right = 157.0 + +[node name="AnimateMenu" parent="VBoxContainer/AnimationOptions/PanelContainer" index="0"] +margin_right = 88.0 + +[node name="InitalButton" parent="VBoxContainer/AnimationOptions" index="2"] +margin_left = 161.0 +margin_right = 278.0 + +[connection signal="value_changed" from="VBoxContainer/LevelsSlider" to="." method="_on_LevelsSlider_value_changed"] +[connection signal="value_changed" from="VBoxContainer/DitherSlider" to="." method="_on_DitherSlider_value_changed"] diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index 90a5d16f9..6147882b9 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -285,6 +285,7 @@ func _setup_image_menu() -> void: "Adjust Hue/Saturation/Value", "Gradient", "Gradient Map", + "Posterize", # "Shader" ] var image_menu: PopupMenu = image_menu_button.get_popup() @@ -680,6 +681,9 @@ func image_menu_id_pressed(id: int) -> void: Global.ImageMenu.GRADIENT_MAP: _popup_dialog(Global.control.get_node("Dialogs/ImageEffects/GradientMapDialog")) + Global.ImageMenu.POSTERIZE: + _popup_dialog(Global.control.get_node("Dialogs/ImageEffects/Posterize")) + # Global.ImageMenu.SHADER: # _popup_dialog(Global.control.get_node("Dialogs/ImageEffects/ShaderEffect"))