diff --git a/README.md b/README.md index 5c2e0a9..4c3496d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ shader_type canvas_item; #include "res://shaderlib/hsv.gdshaderinc" -//!load ./swamp.jpg +//!load ./examples/images/swamp.jpg void fragment() { COLOR = hsv_offset(COLOR, 0.32, 0.2, 0.0); diff --git a/examples/channel_offset.gdshader b/examples/channel_offset.gdshader index 73915ea..a1ad8b5 100644 --- a/examples/channel_offset.gdshader +++ b/examples/channel_offset.gdshader @@ -1,6 +1,6 @@ shader_type canvas_item; -//!load ./swamp.jpg +//!load ./images/swamp.jpg const vec2 offset_r = vec2(-0.002, -0.002); const vec2 offset_g = vec2(0., 0.); diff --git a/examples/color_and_pixelate.gdshader b/examples/color_and_pixelate.gdshader new file mode 100644 index 0000000..7c2c0ef --- /dev/null +++ b/examples/color_and_pixelate.gdshader @@ -0,0 +1,13 @@ +shader_type canvas_item; + +#include "res://shaderlib/hsv.gdshaderinc" +#include "res://shaderlib/effects.gdshaderinc" + +//!load ./images/swamp.jpg + +void fragment() { + COLOR = pixelate(TEXTURE, UV, 200.0); + vec4 hsv = rgb2hsv(COLOR); + COLOR = hsv_offset(COLOR, 0.65, .42-(hsv.y*.3), -.125); + COLOR = hsv_multiply(COLOR, 1.0, 1.0, 1.25); +} diff --git a/examples/denoise.gdshader b/examples/denoise.gdshader new file mode 100644 index 0000000..09dabb6 --- /dev/null +++ b/examples/denoise.gdshader @@ -0,0 +1,9 @@ +shader_type canvas_item; + +//!load ./images/noisy.png + +#include "res://shaderlib/denoise.gdshaderinc" + +void fragment() { + COLOR = smart_denoise(TEXTURE, UV, 12.0, 1.0, .12); +} diff --git a/examples/greyscale.gdshader b/examples/greyscale.gdshader index 921c591..00aac8b 100644 --- a/examples/greyscale.gdshader +++ b/examples/greyscale.gdshader @@ -1,6 +1,6 @@ shader_type canvas_item; -//!load ./swamp.jpg +//!load ./images/swamp.jpg void fragment() { float b = (COLOR.r + COLOR.g + COLOR.b) / 3.0; diff --git a/examples/hsv.gdshader b/examples/hsv.gdshader deleted file mode 100644 index b4d9100..0000000 --- a/examples/hsv.gdshader +++ /dev/null @@ -1,9 +0,0 @@ -shader_type canvas_item; - -#include "res://shaderlib/hsv.gdshaderinc" - -//!load ./swamp.jpg - -void fragment() { - COLOR = hsv_multiply(hsv_offset(COLOR, -0.3, 0.9, 0.0), 1.0, 0.44, 1.0); -} diff --git a/examples/CREDITS.md b/examples/images/CREDITS.md similarity index 100% rename from examples/CREDITS.md rename to examples/images/CREDITS.md diff --git a/examples/grass.png b/examples/images/grass.png similarity index 100% rename from examples/grass.png rename to examples/images/grass.png diff --git a/examples/grass.png.import b/examples/images/grass.png.import similarity index 72% rename from examples/grass.png.import rename to examples/images/grass.png.import index f9a52a6..327f716 100644 --- a/examples/grass.png.import +++ b/examples/images/grass.png.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://c1mh1d2f3u4ju" -path="res://.godot/imported/grass.png-30af7213ed2d70b810b4ae788314a9e9.ctex" +path="res://.godot/imported/grass.png-61a458998da568ce60ccb8a0c7caaf6d.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://examples/grass.png" -dest_files=["res://.godot/imported/grass.png-30af7213ed2d70b810b4ae788314a9e9.ctex"] +source_file="res://examples/images/grass.png" +dest_files=["res://.godot/imported/grass.png-61a458998da568ce60ccb8a0c7caaf6d.ctex"] [params] diff --git a/examples/images/noisy.png b/examples/images/noisy.png new file mode 100644 index 0000000..08e0664 Binary files /dev/null and b/examples/images/noisy.png differ diff --git a/examples/images/noisy.png.import b/examples/images/noisy.png.import new file mode 100644 index 0000000..e8bab73 --- /dev/null +++ b/examples/images/noisy.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cfe2d0qes5x87" +path="res://.godot/imported/noisy.png-1b2e79340785c6c0f50d5bad5ce97356.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://examples/images/noisy.png" +dest_files=["res://.godot/imported/noisy.png-1b2e79340785c6c0f50d5bad5ce97356.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/examples/swamp.jpg b/examples/images/swamp.jpg similarity index 100% rename from examples/swamp.jpg rename to examples/images/swamp.jpg diff --git a/examples/swamp.jpg.import b/examples/images/swamp.jpg.import similarity index 72% rename from examples/swamp.jpg.import rename to examples/images/swamp.jpg.import index 3cc2a0b..e0efe23 100644 --- a/examples/swamp.jpg.import +++ b/examples/images/swamp.jpg.import @@ -3,15 +3,15 @@ importer="texture" type="CompressedTexture2D" uid="uid://ckjb0agn5btv7" -path="res://.godot/imported/swamp.jpg-8e3eac7e7aacce65638e712310cdb35c.ctex" +path="res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex" metadata={ "vram_texture": false } [deps] -source_file="res://examples/swamp.jpg" -dest_files=["res://.godot/imported/swamp.jpg-8e3eac7e7aacce65638e712310cdb35c.ctex"] +source_file="res://examples/images/swamp.jpg" +dest_files=["res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex"] [params] diff --git a/examples/lowpass.gdshader b/examples/lowpass.gdshader index d2d82c5..a84c2a2 100644 --- a/examples/lowpass.gdshader +++ b/examples/lowpass.gdshader @@ -1,15 +1,13 @@ shader_type canvas_item; -//!load ./swamp.jpg +//!load ./images/swamp.jpg // Settings -const float threshold = 0.5; +const float threshold = 0.6; // void fragment() { vec4 tex = texture(TEXTURE , UV); - COLOR.r = min(tex.r, threshold); - COLOR.g = min(tex.g, threshold); - COLOR.b = min(tex.b, threshold); + COLOR.rgb = min(tex.rgb, vec3(threshold)); COLOR.a = tex.a; } diff --git a/examples/multistep_distort.gdshader b/examples/multistep_distort.gdshader index d4cc0ba..cea638f 100644 --- a/examples/multistep_distort.gdshader +++ b/examples/multistep_distort.gdshader @@ -1,7 +1,7 @@ shader_type canvas_item; //!steps 9 -//!load ./swamp.jpg +//!load ./images/swamp.jpg const float strength = 0.01; diff --git a/examples/place_texture.gdshader b/examples/place_texture.gdshader index 282ee69..183d080 100644 --- a/examples/place_texture.gdshader +++ b/examples/place_texture.gdshader @@ -3,8 +3,8 @@ shader_type canvas_item; #include "res://shaderlib/transform.gdshaderinc" #include "res://shaderlib/transparency.gdshaderinc" -//!load ./swamp.jpg -//!load+ img2 ./grass.png +//!load ./images/swamp.jpg +//!load+ img2 ./images/grass.png uniform sampler2D img2: repeat_disable, filter_nearest; diff --git a/project.godot b/project.godot index 85223c5..c244ec2 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Fragmented" -config/version="v6.1" +config/version="v6.2" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.3", "Mobile") run/low_processor_mode=true diff --git a/scenes/ui_container.tscn b/scenes/ui_container.tscn index bc8d37f..6be4c94 100644 --- a/scenes/ui_container.tscn +++ b/scenes/ui_container.tscn @@ -134,6 +134,7 @@ offset_bottom = 32.0 text = "Save As" [node name="SaveImageButton" type="Button" parent="Editor"] +unique_name_in_owner = true layout_mode = 1 anchors_preset = 1 anchor_left = 1.0 @@ -141,6 +142,7 @@ anchor_right = 1.0 offset_left = -72.0 offset_bottom = 32.0 grow_horizontal = 0 +disabled = true text = "Export" [node name="FitImageButton" type="Button" parent="Editor"] diff --git a/shaderlib/denoise.gdshaderinc b/shaderlib/denoise.gdshaderinc new file mode 100644 index 0000000..322f6ec --- /dev/null +++ b/shaderlib/denoise.gdshaderinc @@ -0,0 +1,66 @@ + +/* glslSmartDenoise by Michele Morrone, adapted +original code: https://github.com/BrutPitt/glslSmartDeNoise +license of the original code: +BSD 2-Clause License + +Copyright (c) 2019-2020 Michele Morrone +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#define INV_SQRT_OF_2PI 0.39894228040143267793994605993439 // 1.0/SQRT_OF_2PI +#define INV_PI 0.31830988618379067153776752674503 + +vec4 smart_denoise(sampler2D tex, vec2 uv, float sigma, float kSigma, float threshold) { + float radius = round(kSigma*sigma); + float radQ = radius * radius; + + float invSigmaQx2 = .5 / (sigma * sigma); // 1.0 / (sigma^2 * 2.0) + float invSigmaQx2PI = INV_PI * invSigmaQx2; // 1/(2 * PI * sigma^2) + + float invThresholdSqx2 = .5 / (threshold * threshold); // 1.0 / (sigma^2 * 2.0) + float invThresholdSqrt2PI = INV_SQRT_OF_2PI / threshold; // 1.0 / (sqrt(2*PI) * sigma^2) + + vec4 centrPx = texture(tex,uv); + + float zBuff = 0.0; + vec4 aBuff = vec4(0.0); + vec2 size = vec2(textureSize(tex, 0)); + + for (float dx = -radius; dx <= radius; dx++) { + float pt = sqrt(radQ - dx * dx); // pt = yRadius: have circular trend + for (float dy = -pt; dy <= pt; dy++) { + vec2 d = vec2(dx, dy); + float blurFactor = exp( -dot(d, d) * invSigmaQx2 ) * invSigmaQx2PI; + + vec4 walkPx = texture(tex,uv+d/size); + vec4 dC = walkPx-centrPx; + float deltaFactor = exp(-dot(dC, dC) * invThresholdSqx2) * invThresholdSqrt2PI * blurFactor; + + zBuff += deltaFactor; + aBuff += deltaFactor*walkPx; + } + } + return aBuff/zBuff; +} diff --git a/shaderlib/effects.gdshaderinc b/shaderlib/effects.gdshaderinc new file mode 100644 index 0000000..d9b91f1 --- /dev/null +++ b/shaderlib/effects.gdshaderinc @@ -0,0 +1,14 @@ + +// pixelate by lowering uv resolution +vec4 pixelate(sampler2D tex, vec2 uv, float resolution_x) { + vec2 texture_size = vec2(textureSize(tex, 0)); + vec2 ratio; + if (texture_size.x > texture_size.y) { + ratio = vec2(texture_size.x / texture_size.y, 1.0); + } + else { + ratio = vec2(1.0, texture_size.y / texture_size.x); + } + vec2 r = ratio * resolution_x; + return texture(tex, trunc(uv * r) / r); +} diff --git a/src/Compositor.gd b/src/Compositor.gd index acc781a..0a8bf14 100644 --- a/src/Compositor.gd +++ b/src/Compositor.gd @@ -73,17 +73,17 @@ func update() -> Array: # returns error messages (strings) mat.set_shader_parameter( key, # uniform param name Filesystem.additional_images[key]) + # assign material + image_sprite.material = mat # iterate n times for i in range(steps): # set STEP param mat.set_shader_parameter("STEP", i) - # assign material - image_sprite.material = mat # Get viewport texture - await RenderingServer.frame_post_draw # for good measure + await RenderingServer.frame_post_draw # wait for next frame to get drawn Filesystem.result = get_texture().get_image() - image_sprite.material = null image_sprite.texture = ImageTexture.create_from_image(Filesystem.result) + image_sprite.material = null if fit_image: camera.fit_image() camera.update_vd_zoomlevel() diff --git a/src/Editor.gd b/src/Editor.gd index 69f9aab..edc4633 100644 --- a/src/Editor.gd +++ b/src/Editor.gd @@ -6,6 +6,8 @@ extends Control @onready var save_shader_dialog = %SaveShaderDialog @onready var ui_control_filesave = %SaveImageDialog +@onready var save_image_button = %SaveImageButton + @onready var status_indicator = %StatusIndicator @onready var error_msg_dialog = %ErrorMessageDialog @@ -121,16 +123,24 @@ const gdshader_builtins = [ "POINT_COORD", "SPECULAR_SHININESS" ] +const gdshader_preprocessor = [ + "define", "undef", "include", "pragma", + "if", "elif", "ifdef", "ifndef", "else", "endif" +] # shaderlib var shaderlib_regex = { "hsv": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/hsv\.gdshaderinc\"'), "transform": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/transform\.gdshaderinc\"'), - "transparency": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/transparency\.gdshaderinc\"') + "transparency": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/transparency\.gdshaderinc\"'), + "effects": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/effects\.gdshaderinc\"'), + "denoise": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/denoise\.gdshaderinc\"') } const shaderlib_functions = { "hsv": ["rgb2hsv", "hsv2rgb", "hsv_offset", "hsv_multiply"], "transform": ["place_texture"], "transparency": ["alpha_blend"], + "effects": ["pixelate"], + "denoise": ["smart_denoise"] } # # configure Highlighter @@ -182,6 +192,9 @@ func _on_code_edit_code_completion_requested(): for k in gdshader_builtin_functions + gdshader_sub_functions: code_editor.code_completion_prefixes.append(k) code_editor.add_code_completion_option(CodeEdit.KIND_FUNCTION, k, k+"(", Color.INDIAN_RED) + for k in gdshader_preprocessor: + code_editor.code_completion_prefixes.append(k) + code_editor.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, "#" + k, k) # shaderlib # var shader_code = code_editor.text for key in shaderlib_regex: @@ -257,12 +270,14 @@ func _on_fit_image_button_pressed(): camera.fit_image() func _on_apply_shader_button_pressed(): + save_image_button.disabled = true Filesystem.shader_code = code_editor.text var errors = await compositor.update() if len(errors) > 0: update_status(Status.ERROR, "\n".join(errors)) else: update_status(Status.OKAY) + save_image_button.disabled = false func _on_save_image_button_pressed(): if Filesystem.result != null: diff --git a/src/Filesystem.gd b/src/Filesystem.gd index 2ead478..0fb9507 100644 --- a/src/Filesystem.gd +++ b/src/Filesystem.gd @@ -61,15 +61,30 @@ func save_result(path: String): func load_shader(path: String): print("Load ", path) var file = FileAccess.open(path, FileAccess.READ) - self.shader_code = file.get_as_text() - if "/" in path: # update current working directory - self.cwd = path.substr(0, path.rfind("/")) - self.last_shader_savepath = path + if file != null: + self.shader_code = file.get_as_text() + if "/" in path: # update current working directory + self.cwd = path.substr(0, path.rfind("/")) + self.last_shader_savepath = path + store_last_opened_file() func save_shader(path: String): print("Save ", path) var file = FileAccess.open(path, FileAccess.WRITE) file.store_string(self.shader_code) + file.flush() if "/" in path: # update current working directory self.cwd = path.substr(0, path.rfind("/")) self.last_shader_savepath = path + store_last_opened_file() + +func store_last_opened_file(): + var f = FileAccess.open("user://last_opened", FileAccess.WRITE) + if f != null: + f.store_pascal_string(last_shader_savepath) + f.flush() + +func remember_last_opened_file(): + var f = FileAccess.open("user://last_opened", FileAccess.READ) + if f != null: + last_shader_savepath = f.get_pascal_string() diff --git a/src/Main.gd b/src/Main.gd index 6c59d07..2acdbf4 100644 --- a/src/Main.gd +++ b/src/Main.gd @@ -1,6 +1,7 @@ extends Node @onready var editor_window = %EditorWindow +@onready var ui_container = %UserInterfaceContainer @onready var app_name = ProjectSettings.get_setting("application/config/name") func _ready(): @@ -11,6 +12,10 @@ func _ready(): editor_window.position.y) get_window().min_size = Vector2i(400, 400) editor_window.min_size = Vector2i(560, 400) + # Load last opened file + Filesystem.remember_last_opened_file() + if Filesystem.last_shader_savepath != "": + ui_container.get_node("Editor")._on_open_shader_dialog_file_selected(Filesystem.last_shader_savepath) func update_title(current_file: String = ""): if current_file == "":