diff --git a/README.md b/README.md index 1803499..a12517b 100644 --- a/README.md +++ b/README.md @@ -62,22 +62,22 @@ Example: //!load ... //!steps 5 -uniform int STEP; // this is mandatory! +uniform int STEP; +uniform int STEPS; void fragment() { if (STEP == 0) { - ... + ... } else if (STEP == 1) { - ... + ... + } else if (STEP == STEPS-1) { + ... } - // ... and so on } ``` ## Shaderlib -> Note: The shaderlib API is still unstable as I am figuring things out. It will be declared stable with version 10. - This repo comes with a (still small) shader library including pre-written functions and more. Have a look at the `shaderlib` folder. @@ -153,5 +153,4 @@ Since version v8.0, you can pass a directory to `--load-image` and `--output`. T ## Known Issues - screen scaling is unsupported; Using screen scaling could lead to an either blurry UI, or no scaling at all -> see #45 -- the shaderlib API is still unstable, this will change with version 10 - commandline interface: `--headless` is not supported diff --git a/examples/multistep_pixelsort.gdshader b/examples/multistep_pixelsort.gdshader new file mode 100644 index 0000000..21ba1b6 --- /dev/null +++ b/examples/multistep_pixelsort.gdshader @@ -0,0 +1,20 @@ +shader_type canvas_item; + +#include "./shaderlib/pixelsort.gdshaderinc" + +//!steps 1500 +uniform int STEP; + +//!load ./images/mountain.jpg + +void fragment() { + // pixel sorting works in multiple steps + COLOR = pixelsort_step( + TEXTURE, UV, + DIRECTION_BOTTOM_TO_TOP, + COLOR_MODE_OKLCH, + {true, false, false}, + {-INF, .007, -INF}, + {INF, INF, INF}, + STEP); +} diff --git a/examples/multistep_pixelsort.gdshader.uid b/examples/multistep_pixelsort.gdshader.uid new file mode 100644 index 0000000..47eaaf5 --- /dev/null +++ b/examples/multistep_pixelsort.gdshader.uid @@ -0,0 +1 @@ +uid://csk0fg4by651b diff --git a/examples/project.godot_ b/examples/project.godot_ index bd350d3..5eff383 100644 --- a/examples/project.godot_ +++ b/examples/project.godot_ @@ -11,6 +11,5 @@ config_version=5 [application] config/name="Fragmented Project" -config/features=PackedStringArray("4.3", "Forward Plus") run/main_scene="res://0_empty.tscn" - +config/features=PackedStringArray("4.4", "Forward Plus") diff --git a/examples/sobel.gdshader b/examples/sobel.gdshader new file mode 100644 index 0000000..3a7a8b5 --- /dev/null +++ b/examples/sobel.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; + +//!load ./images/noisy.png + +#include "./shaderlib/sobel.gdshaderinc" + +void fragment() { + // Sobel Filter + COLOR = sobel(TEXTURE, UV); +} diff --git a/examples/sobel.gdshader.uid b/examples/sobel.gdshader.uid new file mode 100644 index 0000000..33b657a --- /dev/null +++ b/examples/sobel.gdshader.uid @@ -0,0 +1 @@ +uid://h376mk1fq4ky diff --git a/export_presets.cfg b/export_presets.cfg index 4b10743..79a474d 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -10,8 +10,10 @@ export_filter="all_resources" include_filter="" exclude_filter="screenshot.png, examples/*, shaderlib/*, tools/*, build-template/*" export_path="dist/Fragmented.x86_64" +patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" +seed=0 encrypt_pck=false encrypt_directory=false script_export_mode=2 diff --git a/project.godot b/project.godot index 796cd4c..95da5af 100644 --- a/project.godot +++ b/project.godot @@ -11,10 +11,9 @@ config_version=5 [application] config/name="Fragmented" -config/version="v9.1" +config/version="v10.2" run/main_scene="res://src/scenes/main.tscn" config/features=PackedStringArray("4.4", "Mobile") -run/low_processor_mode=true config/icon="res://src/assets/icon.png" [autoload] @@ -28,7 +27,6 @@ window/size/viewport_width=640 window/size/viewport_height=672 window/energy_saving/keep_screen_on=false window/subwindows/embed_subwindows=false -window/vsync/vsync_mode=0 [editor_plugins] diff --git a/shaderlib/common.gdshaderinc b/shaderlib/common.gdshaderinc index 8af35dc..9352d6a 100644 --- a/shaderlib/common.gdshaderinc +++ b/shaderlib/common.gdshaderinc @@ -13,3 +13,15 @@ vec4 alpha_blend(vec4 b, vec4 a) { vec3 col = ((a.rgb*a.a) + ((b.rgb*b.a) * (1.0 - a.a)) / alpha); return vec4(col.r, col.g, col.b, alpha); } + +/* + Rotate UV +*/ + +vec2 rotateUV(vec2 uv, float rotation, vec2 center) { + float cosRot = cos(rotation); + float sinRot = sin(rotation); + return vec2( + cosRot * (uv.x - center.x) + sinRot * (uv.y - center.y) + center.x, + cosRot * (uv.y - center.y) - sinRot * (uv.x - center.x) + center.y); +} diff --git a/shaderlib/oklab.gdshaderinc b/shaderlib/oklab.gdshaderinc index 833db27..5a456c7 100644 --- a/shaderlib/oklab.gdshaderinc +++ b/shaderlib/oklab.gdshaderinc @@ -12,6 +12,8 @@ #include "./common.gdshaderinc" vec4 rgb2oklab(vec4 c) { + // oklab.x and .y (a and b) should range from -0.5 to 0.5 + float l = 0.4122214708f * c.r + 0.5363325363f * c.g + 0.0514459929f * c.b; float m = 0.2119034982f * c.r + 0.6806995451f * c.g + 0.1073969566f * c.b; float s = 0.0883024619f * c.r + 0.2817188376f * c.g + 0.6299787005f * c.b; @@ -29,6 +31,8 @@ vec4 rgb2oklab(vec4 c) { } vec4 oklab2rgb(vec4 c) { + // oklab.x and .y (a and b) should range from -0.5 to 0.5 + float l_ = c.x + 0.3963377774f * c.y + 0.2158037573f * c.z; float m_ = c.x - 0.1055613458f * c.y - 0.0638541728f * c.z; float s_ = c.x - 0.0894841775f * c.y - 1.2914855480f * c.z; @@ -46,6 +50,7 @@ vec4 oklab2rgb(vec4 c) { } vec4 oklab2oklch(vec4 c) { + // oklch.z (hue) ranges from -3.6 to 3.6 return vec4( c.x, sqrt((c.y * c.y) + (c.z * c.z)), @@ -55,6 +60,7 @@ vec4 oklab2oklch(vec4 c) { } vec4 oklch2oklab(vec4 c) { + // oklch.z (hue) ranges from -3.6 to 3.6 return vec4( c.x, c.y * cos(c.z), diff --git a/shaderlib/pixelsort.gdshaderinc b/shaderlib/pixelsort.gdshaderinc new file mode 100644 index 0000000..73ed5da --- /dev/null +++ b/shaderlib/pixelsort.gdshaderinc @@ -0,0 +1,126 @@ + +/* + Pixelsorting using odd-even sort + + I roughly followed https://ciphrd.com/2020/04/08/pixel-sorting-on-shader-using-well-crafted-sorting-filters-glsl/ + - vector fields aren't implemented, diagonal sorting is not supported! +*/ + +#include "./hsv.gdshaderinc" +#include "./oklab.gdshaderinc" + +#define INF (1.0/0.0) + +#define DIRECTION_LEFT_TO_RIGHT vec2(1, 0) +#define DIRECTION_RIGHT_TO_LEFT vec2(-1, 0) +#define DIRECTION_TOP_TO_BOTTOM vec2(0, 1) +#define DIRECTION_BOTTOM_TO_TOP vec2(0, -1) + +#define COLOR_MODE_RGB 0 +#define COLOR_MODE_OKLAB 1 +#define COLOR_MODE_OKLCH 2 +#define COLOR_MODE_HSV 3 + +vec4 pixelsort_step( + sampler2D tex, vec2 uv, + vec2 direction, // e.g. (1, 0) for left-to-right or (0, -1) for bottom-to-top + // see DIRECTION_LEFT_TO_RIGHT, etc. + // note: vertical sorting doesn't work, so using e.g. (1, 1) won't work + int color_mode, // 0 = RGB, 1 = OKLAB, 2 = OKLCH, 3 = HSV + // see COLOR_MODE_RGB, etc. + bool color_channel_mask[3], // which color channel(s) to take into account + float lower_threshold[3], // lower threshold for pixels to be considered sorted + // when in doubt, use {-INF, -INF, -INF} + float upper_threshold[3], // upper threshold; {INF, INF, INF} + int step_ // from STEP +) { + // sanitize inputs + direction = clamp(direction, vec2(-1, -1), vec2(1, 1)); + color_mode = clamp(color_mode, 0, 3); + // get neighbour + vec2 texture_size = vec2(textureSize(tex, 0)); + vec2 a = (mod(floor(uv * texture_size), 2.0) * 2.0 - 1.0) * (mod(float(step_), 2.0) * 2.0 - 1.0); + vec2 neighbour_uv = uv + (direction * a / texture_size); + // + vec4 x = texture(tex, uv); + vec4 y = texture(tex, neighbour_uv); + if ( // stop at borders + neighbour_uv.x > 1.0 || + neighbour_uv.x < 0.0 || + neighbour_uv.y > 1.0 || + neighbour_uv.y < 0.0 + ) { + return x; + } else { + // convert color if necessary + // get value to compare + float vx = 0.0; + float vy = 0.0; + vec3 color_x; + vec3 color_y; + if (color_mode == COLOR_MODE_RGB) { + color_x = x.rgb; + color_y = y.rgb; + } else if (color_mode == COLOR_MODE_OKLAB) { + color_x = rgb2oklab(x).rgb; + color_y = rgb2oklab(y).rgb; + } else if (color_mode == COLOR_MODE_OKLCH) { + color_x = oklab2oklch(rgb2oklab(x)).rgb; + color_y = oklab2oklch(rgb2oklab(y)).rgb; + } else if (color_mode == COLOR_MODE_HSV) { + color_x = rgb2hsv(x).rgb; + color_y = rgb2hsv(y).rgb; + } + float divisor = 0.0; + if (color_channel_mask[0]) { + vx += color_x.r; + vy += color_y.r; + divisor += 1.0; + } + if (color_channel_mask[1]) { + vx += color_x.g; + vy += color_y.g; + divisor += 1.0; + } + if (color_channel_mask[2]) { + vx += color_x.b; + vy += color_y.b; + divisor += 1.0; + } + divisor = max(divisor, 1.0); + vx /= divisor; + vy /= divisor; + // + if ( + (a.x < .0 && abs(direction).y == .0) || + (a.y < .0 && abs(direction).x == .0) + ) { + if ( + vy > vx && + // threshold + color_x.r < upper_threshold[0] && + color_x.g < upper_threshold[1] && + color_x.b < upper_threshold[2] && + color_x.r > lower_threshold[0] && + color_x.g > lower_threshold[1] && + color_x.b > lower_threshold[2] + ) { return y; } + else { return x; } + } else if ( + (a.x > .0 && abs(direction).y == .0) || + (a.y > .0 && abs(direction).x == .0) + ) { + if ( + vx >= vy && + // threshold + color_y.r < upper_threshold[0] && + color_y.g < upper_threshold[1] && + color_y.b < upper_threshold[2] && + color_y.r > lower_threshold[0] && + color_y.g > lower_threshold[1] && + color_y.b > lower_threshold[2] + ) { return y; } + else { return x; } + } + } +} diff --git a/shaderlib/pixelsort.gdshaderinc.uid b/shaderlib/pixelsort.gdshaderinc.uid new file mode 100644 index 0000000..e02f48e --- /dev/null +++ b/shaderlib/pixelsort.gdshaderinc.uid @@ -0,0 +1 @@ +uid://doefnwk3vyr0o diff --git a/shaderlib/sobel.gdshaderinc b/shaderlib/sobel.gdshaderinc new file mode 100644 index 0000000..dd665b3 --- /dev/null +++ b/shaderlib/sobel.gdshaderinc @@ -0,0 +1,50 @@ + +/* + Edge Detection (Sobel Filter and Gaussian Blur) by FencerDevLog, adapted + original code: https://godotshaders.com/shader/edge-detection-sobel-filter-and-gaussian-blur/ + license of the original code: CC0 +*/ + +vec3 _convolution(sampler2D tex, vec2 uv, vec2 pixel_size) { + vec3 conv = vec3(0.0); + // Gaussian blur kernel + float gauss[25] = { + 0.00390625, 0.015625, 0.0234375, 0.015625, 0.00390625, + 0.015625, 0.0625, 0.09375, 0.0625, 0.015625, + 0.0234375, 0.09375, 0.140625, 0.09375, 0.0234375, + 0.015625, 0.0625, 0.09375, 0.0625, 0.015625, + 0.00390625, 0.015625, 0.0234375, 0.015625, 0.00390625 + }; + for (int row = 0; row < 5; row++) { + for (int col = 0; col < 5; col++) { + conv += texture(tex, uv + vec2(float(col - 2), float(row - 2)) * pixel_size).rgb * gauss[row * 5 + col]; + } + } + return conv; +} + +vec4 sobel(sampler2D tex, vec2 uv) { + vec2 pixel_size = 1.0/vec2(textureSize(tex, 0)); + vec3 pixels[9]; // Sobel kernel + // [0, 1, 2] + // [3, 4, 5] + // [6, 7, 8] + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 3; col++) { + vec2 uv_ = uv + vec2(float(col - 1), float(row - 1)) * pixel_size; + pixels[row * 3 + col] = _convolution(tex, uv_, pixel_size); + } + } + + // Sobel operator + vec3 gx = ( + pixels[0] * -1.0 + pixels[3] * -2.0 + pixels[6] * -1.0 + + pixels[2] * 1.0 + pixels[5] * 2.0 + pixels[8] * 1.0 + ); + vec3 gy = ( + pixels[0] * -1.0 + pixels[1] * -2.0 + pixels[2] * -1.0 + + pixels[6] * 1.0 + pixels[7] * 2.0 + pixels[8] * 1.0 + ); + vec3 sobel = sqrt(gx * gx + gy * gy); + return vec4(sobel, 1.0); +} \ No newline at end of file diff --git a/shaderlib/sobel.gdshaderinc.uid b/shaderlib/sobel.gdshaderinc.uid new file mode 100644 index 0000000..5753e70 --- /dev/null +++ b/shaderlib/sobel.gdshaderinc.uid @@ -0,0 +1 @@ +uid://bqo1fpunnl05f diff --git a/src/Camera.gd b/src/Camera.gd index b1fe7f1..416e035 100644 --- a/src/Camera.gd +++ b/src/Camera.gd @@ -18,7 +18,7 @@ func _input(event): var old_zoom = self.zoom -func _process(delta: float) -> void: +func _process(_delta: float) -> void: if self.zoom != old_zoom: image_viewport_display.update_zoom_texture_filter(self.zoom) image_viewport_display.material.set_shader_parameter("zoom_level", self.zoom) diff --git a/src/ImageCompositor.gd b/src/ImageCompositor.gd index f913a8a..9f3e95a 100644 --- a/src/ImageCompositor.gd +++ b/src/ImageCompositor.gd @@ -37,19 +37,26 @@ func validate_shader_compilation(shader: Shader) -> bool: # test if uniform list is empty -> if it is empty, the shader compilation failed return len(shader.get_shader_uniform_list()) > 0 -func shader_has_step_uniform(shader: Shader) -> bool: +func shader_has_uniform(shader: Shader, name: String, type: int) -> bool: for u in shader.get_shader_uniform_list(): - if u["name"] == "STEP" && u["type"] == 2: + if u["name"] == name && u["type"] == type: return true return false +func set_vsync(enabled: bool): + if enabled: + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED) + else: + DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED) + func update(overwrite_image_path: String = "") -> Array: # returns error messages (strings) var shader = Filesystem.shader # read from disk if shader == null: return ["No shader opened!"] # get number of steps & check if code has STEP uniform var steps: int = ShaderDirectiveParser.parse_steps_directive(shader.code) - var has_step_uniform: bool = shader_has_step_uniform(shader) + var has_step_uniform: bool = shader_has_uniform(shader, "STEP", 2) + var has_steps_uniform: bool = shader_has_uniform(shader, "STEPS", 2) # validate shader if not validate_shader_compilation(shader): return ["Shader compilation failed!"] @@ -84,6 +91,9 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message image_sprite.texture = Filesystem.original_image image_sprite.offset = Filesystem.original_image.get_size() / 2 self.size = Filesystem.original_image.get_size() + # already show the image viewport & fit the image + if fit_image: camera.fit_image() + image_viewport_display.show() # create shader material var mat = ShaderMaterial.new() mat.shader = shader @@ -95,6 +105,10 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message # assign material image_sprite.material = mat # iterate n times + set_vsync(false) # speed up processing + if has_steps_uniform: + # set STEPS param + mat.set_shader_parameter("STEPS", steps) for i in range(steps): if has_step_uniform: # set STEP param @@ -103,9 +117,7 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message await RenderingServer.frame_post_draw # wait for next frame to get drawn Filesystem.result = get_texture().get_image() image_sprite.texture = ImageTexture.create_from_image(Filesystem.result) + set_vsync(true) # reenable vsync image_sprite.material = null - if fit_image: - camera.fit_image() - image_viewport_display.show() # done return errors diff --git a/src/MainUI.gd b/src/MainUI.gd index 3727f08..8a62ff1 100644 --- a/src/MainUI.gd +++ b/src/MainUI.gd @@ -72,6 +72,7 @@ func _on_open_shader_dialog_confirmed() -> void: func _on_save_image_dialog_file_selected(path): Filesystem.save_result(path) + set_buttons_disabled(false) func _on_save_image_dialog_canceled() -> void: set_buttons_disabled(false)