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

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.
This commit is contained in:
Emmanouil Papadeas 2024-12-16 01:18:56 +02:00
parent 8ceeba76c0
commit 39c85c3079
8 changed files with 97 additions and 67 deletions

View file

@ -71,7 +71,7 @@ enum EffectsMenu {
GAUSSIAN_BLUR, GAUSSIAN_BLUR,
GRADIENT, GRADIENT,
GRADIENT_MAP, GRADIENT_MAP,
SHADER LOADED_EFFECTS
} }
## Enumeration of items present in the Select Menu. ## Enumeration of items present in the Select Menu.
enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY } enum SelectMenu { SELECT_ALL, CLEAR_SELECTION, INVERT, TILE_MODE, MODIFY }

View file

@ -3,6 +3,9 @@ extends Node
signal project_saved signal project_saved
signal reference_image_imported 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_dialog_tscn := preload("res://src/UI/Dialogs/ImportPreviewDialog.tscn")
var preview_dialogs := [] ## Array of preview dialogs 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 elif file_ext in ["pck", "zip"]: # Godot resource pack file
Global.control.get_node("Extensions").install_extension(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) var shader := load(file)
if not shader is Shader: if not shader is Shader:
return return
var file_name: String = file.get_file().get_basename() var new_path := SHADERS_DIRECTORY.path_join(file.get_file())
Global.control.find_child("ShaderEffect").change_shader(shader, file_name) DirAccess.copy_absolute(file, new_path)
shader_copied.emit(new_path)
elif file_ext == "mp3": # Audio file elif file_ext == "mp3": # Audio file
open_audio_file(file) open_audio_file(file)

View file

@ -144,7 +144,9 @@ func set_nodes() -> void:
selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox selection_checkbox = $VBoxContainer/OptionsContainer/SelectionCheckBox
affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton affect_option_button = $VBoxContainer/OptionsContainer/AffectOptionButton
animate_panel = $"%AnimatePanel" animate_panel = $"%AnimatePanel"
if is_instance_valid(animate_panel):
animate_panel.image_effect_node = self animate_panel.image_effect_node = self
if is_instance_valid(live_checkbox):
live_checkbox.button_pressed = live_preview live_checkbox.button_pressed = live_preview

View file

@ -232,7 +232,7 @@ func display_effects(cel: BaseCel, image_override: Image = null) -> Image:
return image return image
var image_size := image.get_size() var image_size := image.get_size()
for effect in effects: for effect in effects:
if not effect.enabled: if not effect.enabled or not is_instance_valid(effect.shader):
continue continue
var shader_image_effect := ShaderImageEffect.new() var shader_image_effect := ShaderImageEffect.new()
shader_image_effect.generate_image(image, effect.shader, effect.params, image_size) shader_image_effect.generate_image(image, effect.shader, effect.params, image_size)

View file

@ -3,8 +3,7 @@ extends ImageEffect
var shader: Shader var shader: Shader
var params := {} var params := {}
@onready var shader_loaded_label: Label = $VBoxContainer/ShaderLoadedLabel @onready var shader_params := $VBoxContainer/ShaderParams as VBoxContainer
@onready var shader_params: BoxContainer = $VBoxContainer/ShaderParams
func _about_to_popup() -> void: func _about_to_popup() -> void:
@ -17,36 +16,22 @@ func _about_to_popup() -> void:
super._about_to_popup() super._about_to_popup()
func commit_action(cel: Image, project := Global.current_project) -> void: func set_nodes() -> void:
if !shader: aspect_ratio_container = $VBoxContainer/AspectRatioContainer
return 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() var gen := ShaderImageEffect.new()
gen.generate_image(cel, shader, params, project.size) 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: func change_shader(shader_tmp: Shader, shader_name: String) -> void:
shader = shader_tmp shader = shader_tmp
preview.material.shader = shader_tmp preview.material.shader = shader_tmp
shader_loaded_label.text = tr("Shader loaded:") + " " + shader_name title = shader_name
params.clear() params.clear()
for child in shader_params.get_children(): for child in shader_params.get_children():
child.queue_free() child.queue_free()

View file

@ -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="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"] [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"] [sub_resource type="ShaderMaterial" id="1"]
[node name="ShaderEffect" type="ConfirmationDialog"] [node name="ShaderEffect" type="ConfirmationDialog"]
position = Vector2i(0, 36)
size = Vector2i(612, 350)
script = ExtResource("1") script = ExtResource("1")
[node name="VBoxContainer" type="VBoxContainer" parent="."] [node name="VBoxContainer" type="VBoxContainer" parent="."]
@ -15,17 +17,14 @@ anchor_bottom = 1.0
offset_left = 8.0 offset_left = 8.0
offset_top = 8.0 offset_top = 8.0
offset_right = -8.0 offset_right = -8.0
offset_bottom = -36.0 offset_bottom = -49.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"
[node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"] [node name="AspectRatioContainer" type="AspectRatioContainer" parent="VBoxContainer"]
layout_mode = 2 layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="Preview" type="TextureRect" parent="VBoxContainer/AspectRatioContainer"] [node name="Preview" type="TextureRect" parent="VBoxContainer/AspectRatioContainer"]
texture_filter = 1
material = SubResource("1") material = SubResource("1")
custom_minimum_size = Vector2(200, 200) custom_minimum_size = Vector2(200, 200)
layout_mode = 2 layout_mode = 2
@ -39,22 +38,5 @@ anchors_preset = 0
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 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"] [node name="ShaderParams" type="VBoxContainer" parent="VBoxContainer"]
layout_mode = 2 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"]

View file

@ -37,6 +37,11 @@ var effects: Array[LayerEffect] = [
func _ready() -> void: func _ready() -> void:
for effect in effects: for effect in effects:
effect_list.get_popup().add_item(effect.name) 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) effect_list.get_popup().id_pressed.connect(_on_effect_list_id_pressed)
@ -49,6 +54,7 @@ func _on_about_to_popup() -> void:
var layer := Global.current_project.layers[Global.current_project.current_layer] var layer := Global.current_project.layers[Global.current_project.current_layer]
enabled_button.button_pressed = layer.effects_enabled enabled_button.button_pressed = layer.effects_enabled
for effect in layer.effects: for effect in layer.effects:
if is_instance_valid(effect.shader):
_create_effect_ui(layer, effect) _create_effect_ui(layer, effect)
@ -59,6 +65,14 @@ func _on_visibility_changed() -> void:
child.queue_free() 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: func _on_effect_list_id_pressed(index: int) -> void:
var layer := Global.current_project.layers[Global.current_project.current_layer] var layer := Global.current_project.layers[Global.current_project.current_layer]
var effect := effects[index].duplicate() var effect := effects[index].duplicate()

View file

@ -14,6 +14,7 @@ const HEART_ICON := preload("res://assets/graphics/misc/heart.svg")
var recent_projects := [] var recent_projects := []
var selected_layout := 0 var selected_layout := 0
var zen_mode := false var zen_mode := false
var loaded_effects_submenu: PopupMenu
# Dialogs # Dialogs
var new_image_dialog := Dialog.new("res://src/UI/Dialogs/CreateNewImage.tscn") 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 palettize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PalettizeDialog.tscn")
var pixelize_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/PixelizeDialog.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 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 manage_layouts_dialog := Dialog.new("res://src/UI/Dialogs/ManageLayouts.tscn")
var window_opacity_dialog := Dialog.new("res://src/UI/Dialogs/WindowOpacityDialog.tscn") var window_opacity_dialog := Dialog.new("res://src/UI/Dialogs/WindowOpacityDialog.tscn")
var about_dialog := Dialog.new("res://src/UI/Dialogs/AboutDialog.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: func popup(dialog_size := Vector2i.ZERO) -> void:
if not is_instance_valid(node): if not is_instance_valid(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) var scene := load(scene_path)
if not scene is PackedScene: if not scene is PackedScene:
return return
node = scene.instantiate() node = scene.instantiate()
if not is_instance_valid(node): if is_instance_valid(node):
return
Global.control.get_node("Dialogs").add_child(node) Global.control.get_node("Dialogs").add_child(node)
node.popup_centered(dialog_size)
var is_file_dialog := node is FileDialog
Global.dialog_open(true, is_file_dialog)
func _ready() -> void: func _ready() -> void:
Global.project_switched.connect(_project_switched) Global.project_switched.connect(_project_switched)
Global.cel_switched.connect(_update_current_frame_mark) Global.cel_switched.connect(_update_current_frame_mark)
OpenSave.shader_copied.connect(_load_shader_file)
_setup_file_menu() _setup_file_menu()
_setup_edit_menu() _setup_edit_menu()
_setup_view_menu() _setup_view_menu()
@ -457,15 +461,45 @@ func _setup_effects_menu() -> void:
"Gaussian Blur": "gaussian_blur", "Gaussian Blur": "gaussian_blur",
"Gradient": "gradient", "Gradient": "gradient",
"Gradient Map": "gradient_map", "Gradient Map": "gradient_map",
# "Shader": "" "Loaded": ""
} }
var i := 0 var i := 0
for item in menu_items: for item in menu_items:
if item == "Loaded":
_setup_loaded_effects_submenu()
else:
_set_menu_shortcut(menu_items[item], effects_menu, i, item) _set_menu_shortcut(menu_items[item], effects_menu, i, item)
i += 1 i += 1
effects_menu.id_pressed.connect(effects_menu_id_pressed) 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: func _setup_select_menu() -> void:
# Order as in Global.SelectMenu enum # Order as in Global.SelectMenu enum
var select_menu_items := { 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) 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: func _panels_submenu_id_pressed(id: int) -> void:
if zen_mode: if zen_mode:
return return
@ -950,8 +995,6 @@ func effects_menu_id_pressed(id: int) -> void:
pixelize_dialog.popup() pixelize_dialog.popup()
Global.EffectsMenu.POSTERIZE: Global.EffectsMenu.POSTERIZE:
posterize_dialog.popup() posterize_dialog.popup()
#Global.EffectsMenu.SHADER:
#shader_effect_dialog.popup()
_: _:
_handle_metadata(id, effects_menu) _handle_metadata(id, effects_menu)