From 39c85c3079ad8195c7ee1b926add4ef1fe30978b Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Mon, 16 Dec 2024 01:18:56 +0200 Subject: [PATCH] Implement the ability to load custom shaders and image and layer effects Finally expose the feature of importing custom shaders as image effects, and implement custom shader loading for layer effects as well. To load a shader, drag and drop a .gdshader file into Pixelorama and it will get copied into `user://shaders`. Then, in the Effects menu, a new "Loaded" submenu will appear, and the new shaders will also be available in the layer effects dialog. Since they are stored on a persistent location, the shaders will also be available on the next times Pixelorama will launch as well. --- src/Autoload/Global.gd | 2 +- src/Autoload/OpenSave.gd | 10 ++- src/Classes/ImageEffect.gd | 6 +- src/Classes/Layers/BaseLayer.gd | 2 +- src/UI/Dialogs/ImageEffects/ShaderEffect.gd | 33 +++------ src/UI/Dialogs/ImageEffects/ShaderEffect.tscn | 28 ++------ .../LayerEffects/LayerEffectsSettings.gd | 16 ++++- src/UI/TopMenuContainer/TopMenuContainer.gd | 67 +++++++++++++++---- 8 files changed, 97 insertions(+), 67 deletions(-) diff --git a/src/Autoload/Global.gd b/src/Autoload/Global.gd index 1a02862a9..1c9d1c1fb 100644 --- a/src/Autoload/Global.gd +++ b/src/Autoload/Global.gd @@ -71,7 +71,7 @@ enum EffectsMenu { GAUSSIAN_BLUR, GRADIENT, GRADIENT_MAP, - SHADER + LOADED_EFFECTS } ## Enumeration of items present in the Select Menu. enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY } diff --git a/src/Autoload/OpenSave.gd b/src/Autoload/OpenSave.gd index 868d25ee1..84212ea3e 100644 --- a/src/Autoload/OpenSave.gd +++ b/src/Autoload/OpenSave.gd @@ -3,6 +3,9 @@ extends Node signal project_saved signal reference_image_imported +signal shader_copied(file_path: String) + +const SHADERS_DIRECTORY := "user://shaders" var preview_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn") var preview_dialogs := [] ## Array of preview dialogs @@ -39,12 +42,13 @@ func handle_loading_file(file: String) -> void: elif file_ext in ["pck", "zip"]: # Godot resource pack file Global.control.get_node("Extensions").install_extension(file) - elif file_ext == "shader" or file_ext == "gdshader": # Godot shader file + elif file_ext == "gdshader": # Godot shader file var shader := load(file) if not shader is Shader: return - var file_name: String = file.get_file().get_basename() - Global.control.find_child("ShaderEffect").change_shader(shader, file_name) + var new_path := SHADERS_DIRECTORY.path_join(file.get_file()) + DirAccess.copy_absolute(file, new_path) + shader_copied.emit(new_path) elif file_ext == "mp3": # Audio file open_audio_file(file) diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd index 05abe63df..2811251ee 100644 --- a/src/Classes/ImageEffect.gd +++ b/src/Classes/ImageEffect.gd @@ -144,8 +144,10 @@ func set_nodes() -> void: selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton animate_panel = $"%AnimatePanel" - animate_panel.image_effect_node = self - live_checkbox.button_pressed = live_preview + if is_instance_valid(animate_panel): + animate_panel.image_effect_node = self + if is_instance_valid(live_checkbox): + live_checkbox.button_pressed = live_preview func display_animate_dialog() -> void: diff --git a/src/Classes/Layers/BaseLayer.gd b/src/Classes/Layers/BaseLayer.gd index bdc08e227..a63e4e8dd 100644 --- a/src/Classes/Layers/BaseLayer.gd +++ b/src/Classes/Layers/BaseLayer.gd @@ -232,7 +232,7 @@ func display_effects(cel: BaseCel, image_override: Image = null) -> Image: return image var image_size := image.get_size() for effect in effects: - if not effect.enabled: + if not effect.enabled or not is_instance_valid(effect.shader): continue var shader_image_effect := ShaderImageEffect.new() shader_image_effect.generate_image(image, effect.shader, effect.params, image_size) diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd index bd6524942..76f3687ac 100644 --- a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd +++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd @@ -3,8 +3,7 @@ extends ImageEffect var shader: Shader var params := {} -@onready var shader_loaded_label: Label = $VBoxContainer/ShaderLoadedLabel -@onready var shader_params: BoxContainer = $VBoxContainer/ShaderParams +@onready var shader_params := $VBoxContainer/ShaderParams as VBoxContainer func _about_to_popup() -> void: @@ -17,36 +16,22 @@ func _about_to_popup() -> void: super._about_to_popup() -func commit_action(cel: Image, project := Global.current_project) -> void: - if !shader: - return +func set_nodes() -> void: + aspect_ratio_container = $VBoxContainer/AspectRatioContainer + preview = $VBoxContainer/AspectRatioContainer/Preview + +func commit_action(cel: Image, project := Global.current_project) -> void: + if not is_instance_valid(shader): + return var gen := ShaderImageEffect.new() gen.generate_image(cel, shader, params, project.size) -func _on_ChooseShader_pressed() -> void: - if OS.get_name() == "Web": - Html5FileExchange.load_shader() - else: - $FileDialog.popup_centered(Vector2(300, 340)) - - -func _on_FileDialog_file_selected(path: String) -> void: - var shader_tmp = load(path) - if !shader_tmp is Shader: - return - change_shader(shader_tmp, path.get_file().get_basename()) - - -func set_nodes() -> void: - preview = $VBoxContainer/AspectRatioContainer/Preview - - func change_shader(shader_tmp: Shader, shader_name: String) -> void: shader = shader_tmp preview.material.shader = shader_tmp - shader_loaded_label.text = tr("Shader loaded:") + " " + shader_name + title = shader_name params.clear() for child in shader_params.get_children(): child.queue_free() diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn index 037559f87..a42ddb3f7 100644 --- a/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn +++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=4 format=3 uid="uid://bkr47ocij684y"] +[gd_scene load_steps=4 format=3 uid="uid://b1ola6loro5m7"] [ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/ShaderEffect.gd" id="1"] [ext_resource type="PackedScene" uid="uid://3pmb60gpst7b" path="res://src/UI/Nodes/TransparentChecker.tscn" id="2"] @@ -6,6 +6,8 @@ [sub_resource type="ShaderMaterial" id="1"] [node name="ShaderEffect" type="ConfirmationDialog"] +position = Vector2i(0, 36) +size = Vector2i(612, 350) script = ExtResource("1") [node name="VBoxContainer" type="VBoxContainer" parent="."] @@ -15,17 +17,14 @@ anchor_bottom = 1.0 offset_left = 8.0 offset_top = 8.0 offset_right = -8.0 -offset_bottom = -36.0 - -[node name="Label" type="Label" parent="VBoxContainer"] -layout_mode = 2 -text = "This is an experimental feature and may not be included in the stable version" +offset_bottom = -49.0 [node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"] layout_mode = 2 size_flags_vertical = 3 [node name="Preview" type="TextureRect" parent="VBoxContainer/AspectRatioContainer"] +texture_filter = 1 material = SubResource("1") custom_minimum_size = Vector2(200, 200) layout_mode = 2 @@ -39,22 +38,5 @@ anchors_preset = 0 anchor_right = 1.0 anchor_bottom = 1.0 -[node name="ChooseShader" type="Button" parent="VBoxContainer"] -layout_mode = 2 -mouse_default_cursor_shape = 2 -text = "Choose Shader" - -[node name="ShaderLoadedLabel" type="Label" parent="VBoxContainer"] -layout_mode = 2 -text = "No shader loaded!" - [node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"] layout_mode = 2 - -[node name="FileDialog" type="FileDialog" parent="." groups=["FileDialogs"]] -access = 2 -filters = PackedStringArray("*gdshader; Godot Shader File") -show_hidden_files = true - -[connection signal="pressed" from="VBoxContainer/ChooseShader" to="." method="_on_ChooseShader_pressed"] -[connection signal="file_selected" from="FileDialog" to="." method="_on_FileDialog_file_selected"] diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 8e86da5d6..6979cb998 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -37,6 +37,11 @@ var effects: Array[LayerEffect] = [ func _ready() -> void: for effect in effects: effect_list.get_popup().add_item(effect.name) + if not DirAccess.dir_exists_absolute(OpenSave.SHADERS_DIRECTORY): + DirAccess.make_dir_recursive_absolute(OpenSave.SHADERS_DIRECTORY) + for file_name in DirAccess.get_files_at(OpenSave.SHADERS_DIRECTORY): + _load_shader_file(OpenSave.SHADERS_DIRECTORY.path_join(file_name)) + OpenSave.shader_copied.connect(_load_shader_file) effect_list.get_popup().id_pressed.connect(_on_effect_list_id_pressed) @@ -49,7 +54,8 @@ func _on_about_to_popup() -> void: var layer := Global.current_project.layers[Global.current_project.current_layer] enabled_button.button_pressed = layer.effects_enabled for effect in layer.effects: - _create_effect_ui(layer, effect) + if is_instance_valid(effect.shader): + _create_effect_ui(layer, effect) func _on_visibility_changed() -> void: @@ -59,6 +65,14 @@ func _on_visibility_changed() -> void: child.queue_free() +func _load_shader_file(file_path: String) -> void: + var file := load(file_path) + if file is Shader: + var effect_name := file_path.get_file().get_basename() + effects.append(LayerEffect.new(effect_name, file)) + effect_list.get_popup().add_item(effect_name) + + func _on_effect_list_id_pressed(index: int) -> void: var layer := Global.current_project.layers[Global.current_project.current_layer] var effect := effects[index].duplicate() diff --git a/src/UI/TopMenuContainer/TopMenuContainer.gd b/src/UI/TopMenuContainer/TopMenuContainer.gd index df41698e8..b640dd254 100644 --- a/src/UI/TopMenuContainer/TopMenuContainer.gd +++ b/src/UI/TopMenuContainer/TopMenuContainer.gd @@ -14,6 +14,7 @@ const HEART_ICON := preload("res://assets/graphics/misc/heart.svg") var recent_projects := [] var selected_layout := 0 var zen_mode := false +var loaded_effects_submenu: PopupMenu # Dialogs var new_image_dialog := Dialog.new("res://src/UI/Dialogs/CreateNewImage.tscn") @@ -40,7 +41,7 @@ var gradient_map_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/Gradien 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 loaded_effect_dialogs: Array[Dialog] = [] var manage_layouts_dialog := Dialog.new("res://src/UI/Dialogs/ManageLayouts.tscn") var window_opacity_dialog := Dialog.new("res://src/UI/Dialogs/WindowOpacityDialog.tscn") var about_dialog := Dialog.new("res://src/UI/Dialogs/AboutDialog.tscn") @@ -79,21 +80,24 @@ class Dialog: func popup(dialog_size := Vector2i.ZERO) -> void: if not is_instance_valid(node): - var scene := load(scene_path) - if not scene is PackedScene: - return - node = scene.instantiate() - if not is_instance_valid(node): - return - Global.control.get_node("Dialogs").add_child(node) + instantiate_scene() node.popup_centered(dialog_size) var is_file_dialog := node is FileDialog Global.dialog_open(true, is_file_dialog) + func instantiate_scene() -> void: + var scene := load(scene_path) + if not scene is PackedScene: + return + node = scene.instantiate() + if is_instance_valid(node): + Global.control.get_node("Dialogs").add_child(node) + func _ready() -> void: Global.project_switched.connect(_project_switched) Global.cel_switched.connect(_update_current_frame_mark) + OpenSave.shader_copied.connect(_load_shader_file) _setup_file_menu() _setup_edit_menu() _setup_view_menu() @@ -457,15 +461,45 @@ func _setup_effects_menu() -> void: "Gaussian Blur": "gaussian_blur", "Gradient": "gradient", "Gradient Map": "gradient_map", - # "Shader": "" + "Loaded": "" } var i := 0 for item in menu_items: - _set_menu_shortcut(menu_items[item], effects_menu, i, item) + if item == "Loaded": + _setup_loaded_effects_submenu() + else: + _set_menu_shortcut(menu_items[item], effects_menu, i, item) i += 1 effects_menu.id_pressed.connect(effects_menu_id_pressed) +func _setup_loaded_effects_submenu() -> void: + if not DirAccess.dir_exists_absolute(OpenSave.SHADERS_DIRECTORY): + DirAccess.make_dir_recursive_absolute(OpenSave.SHADERS_DIRECTORY) + var shader_files := DirAccess.get_files_at(OpenSave.SHADERS_DIRECTORY) + if shader_files.size() == 0: + return + for shader_file in shader_files: + _load_shader_file(OpenSave.SHADERS_DIRECTORY.path_join(shader_file)) + + +func _load_shader_file(file_path: String) -> void: + var file := load(file_path) + if file is not Shader: + return + var effect_name := file_path.get_file().get_basename() + if not is_instance_valid(loaded_effects_submenu): + loaded_effects_submenu = PopupMenu.new() + loaded_effects_submenu.set_name("loaded_effects_submenu") + loaded_effects_submenu.id_pressed.connect(_loaded_effects_submenu_id_pressed) + effects_menu.add_child(loaded_effects_submenu) + effects_menu.add_submenu_item("Loaded", loaded_effects_submenu.get_name()) + loaded_effects_submenu.add_item(effect_name) + var effect_index := loaded_effects_submenu.item_count - 1 + loaded_effects_submenu.set_item_metadata(effect_index, file) + loaded_effect_dialogs.append(Dialog.new("res://src/UI/Dialogs/ImageEffects/ShaderEffect.tscn")) + + func _setup_select_menu() -> void: # Order as in Global.SelectMenu enum var select_menu_items := { @@ -770,6 +804,17 @@ func _snap_to_submenu_id_pressed(id: int) -> void: snap_to_submenu.set_item_checked(id, Global.snap_to_perspective_guides) +func _loaded_effects_submenu_id_pressed(id: int) -> void: + var dialog := loaded_effect_dialogs[id] + if is_instance_valid(dialog.node): + dialog.popup() + else: + dialog.instantiate_scene() + var shader := loaded_effects_submenu.get_item_metadata(id) as Shader + dialog.node.change_shader(shader, loaded_effects_submenu.get_item_text(id)) + dialog.popup() + + func _panels_submenu_id_pressed(id: int) -> void: if zen_mode: return @@ -950,8 +995,6 @@ func effects_menu_id_pressed(id: int) -> void: pixelize_dialog.popup() Global.EffectsMenu.POSTERIZE: posterize_dialog.popup() - #Global.EffectsMenu.SHADER: - #shader_effect_dialog.popup() _: _handle_metadata(id, effects_menu)