From b3021ceb671f477a168791674e5255fc71fe9cde Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas <35376950+OverloadedOrama@users.noreply.github.com> Date: Sun, 8 Sep 2024 02:40:28 +0300 Subject: [PATCH] Add a gaussian blur layer effect --- src/Classes/ShaderLoader.gd | 172 +++++++++++------- src/Shaders/Effects/GaussianBlur.gdshader | 134 ++++++++++++++ .../LayerEffects/LayerEffectsSettings.gd | 1 + 3 files changed, 241 insertions(+), 66 deletions(-) create mode 100644 src/Shaders/Effects/GaussianBlur.gdshader diff --git a/src/Classes/ShaderLoader.gd b/src/Classes/ShaderLoader.gd index 8a0f6f31e..1db9d732f 100644 --- a/src/Classes/ShaderLoader.gd +++ b/src/Classes/ShaderLoader.gd @@ -15,24 +15,27 @@ static func create_ui_for_shader_uniforms( ) -> void: var code := shader.code.split("\n") var uniforms: PackedStringArray = [] + var uniform_data: PackedStringArray = [] var description: String = "" - var descriprion_began := false + var description_began := false for line in code: - ## Management of "end" tags + # Management of "end" tags if line.begins_with("// (end DESCRIPTION)"): - descriprion_began = false - if descriprion_began: + description_began = false + if description_began: description += "\n" + line.strip_edges() - ## Detection of uniforms + # Detection of uniforms if line.begins_with("uniform"): uniforms.append(line) + if line.begins_with("// uniform_data"): + uniform_data.append(line) - ## Management of "begin" tags + # Management of "begin" tags elif line.begins_with("// (begin DESCRIPTION)"): - descriprion_began = true - ## Validation of begin/end tags - if descriprion_began == true: ## Description started but never ended. treat it as an error + description_began = true + # Validation of begin/end tags + if description_began == true: # Description started but never ended. treat it as an error print("Shader description started but never finished. Assuming empty description") description = "" if not description.is_empty(): @@ -59,65 +62,102 @@ static func create_ui_for_shader_uniforms( var u_init := u_left_side[0].split(" ") var u_type := u_init[1] var u_name := u_init[2] + # Find custom data of the uniform, if any exists + # Right now it only checks if a uniform should have another type of node + # Such as integers having OptionButtons + # But in the future it could be expanded to include custom names or descriptions. + var custom_data: PackedStringArray = [] + var type_override := "" + for data in uniform_data: + if u_name in data: + var line_to_examine := data.split(" ") + if line_to_examine[3] == "type::": + var temp_splitter := data.split("::") + if temp_splitter.size() > 1: + type_override = temp_splitter[1].strip_edges() + + custom_data.append(data) var humanized_u_name := Keychain.humanize_snake_case(u_name) + ":" if u_type == "float" or u_type == "int": + var hbox := HBoxContainer.new() var label := Label.new() label.text = humanized_u_name label.size_flags_horizontal = Control.SIZE_EXPAND_FILL - var slider := ValueSlider.new() - slider.allow_greater = true - slider.allow_lesser = true - slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL - var min_value := 0.0 - var max_value := 255.0 - var step := 1.0 - var range_values_array: PackedStringArray - if "hint_range" in u_hint: - var range_values: String = u_hint.replace("hint_range(", "") - range_values = range_values.replace(")", "").strip_edges() - range_values_array = range_values.split(",") - - if u_type == "float": - if range_values_array.size() >= 1: - min_value = float(range_values_array[0]) - else: - min_value = 0.01 - - if range_values_array.size() >= 2: - max_value = float(range_values_array[1]) - - if range_values_array.size() >= 3: - step = float(range_values_array[2]) - else: - step = 0.01 - - if u_value != "": - slider.value = float(u_value) - else: - if range_values_array.size() >= 1: - min_value = int(range_values_array[0]) - - if range_values_array.size() >= 2: - max_value = int(range_values_array[1]) - - if range_values_array.size() >= 3: - step = int(range_values_array[2]) - - if u_value != "": - slider.value = int(u_value) - if params.has(u_name): - slider.value = params[u_name] - else: - params[u_name] = slider.value - slider.min_value = min_value - slider.max_value = max_value - slider.step = step - slider.value_changed.connect(value_changed.bind(u_name)) - slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND - var hbox := HBoxContainer.new() hbox.add_child(label) - hbox.add_child(slider) + if type_override.begins_with("OptionButton"): + var option_button := OptionButton.new() + option_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + option_button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND + option_button.item_selected.connect(value_changed.bind(u_name)) + var items := ( + type_override + . replace("OptionButton ", "") + . replace("[", "") + . replace("]", "") + . split("||") + ) + for item in items: + option_button.add_item(item) + if u_value != "": + option_button.select(int(u_value)) + if params.has(u_name): + option_button.select(params[u_name]) + else: + params[u_name] = option_button.selected + hbox.add_child(option_button) + else: + var slider := ValueSlider.new() + slider.allow_greater = true + slider.allow_lesser = true + slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL + var min_value := 0.0 + var max_value := 255.0 + var step := 1.0 + var range_values_array: PackedStringArray + if "hint_range" in u_hint: + var range_values: String = u_hint.replace("hint_range(", "") + range_values = range_values.replace(")", "").strip_edges() + range_values_array = range_values.split(",") + + if u_type == "float": + if range_values_array.size() >= 1: + min_value = float(range_values_array[0]) + else: + min_value = 0.01 + + if range_values_array.size() >= 2: + max_value = float(range_values_array[1]) + + if range_values_array.size() >= 3: + step = float(range_values_array[2]) + else: + step = 0.01 + + if u_value != "": + slider.value = float(u_value) + else: + if range_values_array.size() >= 1: + min_value = int(range_values_array[0]) + + if range_values_array.size() >= 2: + max_value = int(range_values_array[1]) + + if range_values_array.size() >= 3: + step = int(range_values_array[2]) + + if u_value != "": + slider.value = int(u_value) + if params.has(u_name): + slider.value = params[u_name] + else: + params[u_name] = slider.value + slider.min_value = min_value + slider.max_value = max_value + slider.step = step + slider.value_changed.connect(value_changed.bind(u_name)) + slider.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND + hbox.add_child(slider) parent_node.add_child(hbox) elif u_type == "vec2" or u_type == "ivec2" or u_type == "uvec2": var label := Label.new() @@ -352,23 +392,23 @@ static func _get_loaded_texture(params: Dictionary, parameter_name: String) -> I if parameter_name in params: if params[parameter_name] is ImageTexture: return params[parameter_name].get_image() - var image = Image.create_empty(64, 64, false, Image.FORMAT_RGBA8) + var image := Image.create_empty(64, 64, false, Image.FORMAT_RGBA8) return image static func _shader_update_texture( resource_proj: ResourceProject, value_changed: Callable, parameter_name: String ) -> void: - var warnings = "" + var warnings := "" if resource_proj.frames.size() > 1: warnings += "This resource is intended to have 1 frame only. Extra frames will be ignored." if resource_proj.layers.size() > 1: warnings += "\nThis resource is intended to have 1 layer only. layers will be blended." - var updated_image = Image.create_empty( + var updated_image := Image.create_empty( resource_proj.size.x, resource_proj.size.y, false, Image.FORMAT_RGBA8 ) - var frame = resource_proj.frames[0] + var frame := resource_proj.frames[0] DrawingAlgos.blend_layers(updated_image, frame, Vector2i.ZERO, resource_proj) value_changed.call(ImageTexture.create_from_image(updated_image), parameter_name) if not warnings.is_empty(): @@ -378,7 +418,7 @@ static func _shader_update_texture( static func _modify_texture_resource( image: Image, resource_name: StringName, update_callable: Callable ) -> void: - var resource_proj = ResourceProject.new([], resource_name, image.get_size()) + var resource_proj := ResourceProject.new([], resource_name, image.get_size()) resource_proj.layers.append(PixelLayer.new(resource_proj)) resource_proj.frames.append(resource_proj.new_empty_frame()) resource_proj.frames[0].cels[0].set_content(image) diff --git a/src/Shaders/Effects/GaussianBlur.gdshader b/src/Shaders/Effects/GaussianBlur.gdshader new file mode 100644 index 000000000..85581dcd9 --- /dev/null +++ b/src/Shaders/Effects/GaussianBlur.gdshader @@ -0,0 +1,134 @@ +// https://godotshaders.com/shader/gaussian-blur-functions-for-gles2/ +// Licensed under MIT. +shader_type canvas_item; + +// uniform_data blur_type type:: OptionButton [Xor's Gaussian Blur||Monk's Multi-Pass Gaussian Blur||NoDev's Single-Pass Gaussian Blur||NoDev's Multi-Pass Gaussian Blur] +uniform int blur_type : hint_range(0, 3, 1) = 0; +uniform int blur_amount = 16; +uniform float blur_radius = 1.0; +uniform vec2 blur_direction = vec2(1, 1); + +// Xor's gaussian blur function +// Link: https://xorshaders.weebly.com/tutorials/blur-shaders-5-part-2 +// Defaults from: https://www.shadertoy.com/view/Xltfzj +// +// BLUR BLURRINESS (Default 8.0) +// BLUR ITERATIONS (Default 16.0 - More is better but slower) +// BLUR QUALITY (Default 4.0 - More is better but slower) +// +// Desc.: Don't have the best performance but will run on almost +// anything, although, if developing for mobile, is better to use +// 'texture_nodevgaussian(...) instead'. +vec4 texture_xorgaussian(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, int iterations, int quality) { + vec2 radius = blurriness / (1.0 / pixel_size).xy; + vec4 blurred_tex = texture(tex, uv); + + for(float d = 0.0; d < TAU; d += TAU / float(iterations)) { + for(float i = 1.0 / float(quality); i <= 1.0; i += 1.0 / float(quality)) { + vec2 directions = uv + vec2(cos(d), sin(d)) * radius * i; + blurred_tex += texture(tex, directions); + } + } + blurred_tex /= float(quality) * float(iterations) + 1.0; + + return blurred_tex; +} + +// Experience-Monks' fast gaussian blur function +// Link: https://github.com/Experience-Monks/glsl-fast-gaussian-blur/ +// +// BLUR ITERATIONS (Default 16.0 - More is better but slower) +// BLUR DIRECTION (Direction in which the blur is applied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) +// +// Desc.: ACTUALLY PRETTY SLOW but still pretty good for custom cinematic +// bloom effects, since this needs render 2 passes +vec4 texture_monksgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, int iterations, vec2 direction) { + vec4 blurred_tex = vec4(0.0); + vec2 resolution = 1.0 / pixel_size; + + for (int i = 0; i < iterations; i++ ) { + float size = float(iterations - i); + + vec2 off1 = vec2(1.3846153846) * (direction * size); + vec2 off2 = vec2(3.2307692308) * (direction * size); + + blurred_tex += texture(tex, uv) * 0.2270270270; + blurred_tex += texture(tex, uv + (off1 / resolution)) * 0.3162162162; + blurred_tex += texture(tex, uv - (off1 / resolution)) * 0.3162162162; + blurred_tex += texture(tex, uv + (off2 / resolution)) * 0.0702702703; + blurred_tex += texture(tex, uv - (off2 / resolution)) * 0.0702702703; + } + + blurred_tex /= float(iterations) + 1.0; + + return blurred_tex; +} + +// u/_NoDev_'s gaussian blur function +// Discussion Link: https://www.reddit.com/r/godot/comments/klgfo9/help_with_shaders_in_gles2/ +// Code Link: https://postimg.cc/7JDJw80d +// +// BLUR BLURRINESS (Default 8.0 - More is better but slower) +// BLUR RADIUS (Default 1.5) +// BLUR DIRECTION (Direction in which the blur is applied, use vec2(1, 0) for first pass and vec2(0, 1) for second pass) +// +// Desc.: Really fast and GOOD FOR MOST CASES, but might NOT RUN IN THE WEB! +// use 'texture_xorgaussian' instead if you found any issues. +vec4 texture_nodevgaussian_singlepass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, float radius) { + float n = 0.0015; + vec4 blurred_tex = vec4(0); + float weight; + + for (float i = -blurriness; i <= blurriness; i++) { + float d = i / PI; + vec2 anchor = vec2(cos(d), sin(d)) * radius * i; + vec2 directions = uv + pixel_size * anchor; + blurred_tex += texture(tex, directions) * n; + if (i <= 0.0) {n += 0.0015; } + if (i > 0.0) {n -= 0.0015; } + weight += n; + } + + float norm = 1.0 / weight; + blurred_tex *= norm; + return blurred_tex; +} +vec4 texture_nodevgaussian_multipass(sampler2D tex, vec2 uv, vec2 pixel_size, float blurriness, vec2 direction) { + float n = 0.0015; + vec4 blurred_tex = vec4(0); + float weight; + + for (float i = -blurriness; i <= blurriness; i++) { + vec2 directions = uv + pixel_size * (direction * i); + blurred_tex += texture(tex, directions) * n; + if (i <= 0.0) {n += 0.0015; } + if (i > 0.0) {n -= 0.0015; } + weight += n; + } + + float norm = 1.0 / weight; + blurred_tex *= norm; + return blurred_tex; +} + +void fragment() { + if (blur_type == 0) { + vec4 xorgaussian = texture_xorgaussian(TEXTURE, UV, TEXTURE_PIXEL_SIZE, float(blur_amount), 16, 4); + COLOR = xorgaussian; + } + else if (blur_type == 1) { + vec4 monksgaussian_multipass = texture_monksgaussian_multipass(TEXTURE, UV, TEXTURE_PIXEL_SIZE, blur_amount, blur_direction); + COLOR = monksgaussian_multipass; + } + else if (blur_type == 2) { + vec4 nodevgaussian_singlepass = texture_nodevgaussian_singlepass(TEXTURE, UV, TEXTURE_PIXEL_SIZE, float(blur_amount), blur_radius); + COLOR = nodevgaussian_singlepass; + } + else if (blur_type == 3) { + vec4 nodevgaussian_multipass = texture_nodevgaussian_multipass(TEXTURE, UV, TEXTURE_PIXEL_SIZE, float(blur_amount), blur_direction); + COLOR = nodevgaussian_multipass; + } + else { + COLOR = texture(TEXTURE, UV); + } +} diff --git a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd index 3985dc75b..3e035037b 100644 --- a/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd +++ b/src/UI/Timeline/LayerEffects/LayerEffectsSettings.gd @@ -7,6 +7,7 @@ var effects: Array[LayerEffect] = [ LayerEffect.new( "Convolution Matrix", preload("res://src/Shaders/Effects/ConvolutionMatrix.gdshader") ), + LayerEffect.new("Gaussian Blur", preload("res://src/Shaders/Effects/GaussianBlur.gdshader")), LayerEffect.new("Offset", preload("res://src/Shaders/Effects/OffsetPixels.gdshader")), LayerEffect.new("Outline", preload("res://src/Shaders/Effects/OutlineInline.gdshader")), LayerEffect.new("Drop Shadow", preload("res://src/Shaders/Effects/DropShadow.gdshader")),