From 4278cde44b9eaa781bd835309f4c588ed3449897 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 31 Mar 2025 15:37:55 +0200 Subject: [PATCH 01/11] Disable low_processor_mode in the settings to improve multistep performance --- project.godot | 1 - 1 file changed, 1 deletion(-) diff --git a/project.godot b/project.godot index 4d4aadc..918c937 100644 --- a/project.godot +++ b/project.godot @@ -14,7 +14,6 @@ config/name="Fragmented" config/version="v10.0" 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] From 9d23efea63736b4ddaed98f45047c957b42c9c4a Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 31 Mar 2025 15:39:36 +0200 Subject: [PATCH 02/11] Fix: Show and align image viewport before first step --- src/ImageCompositor.gd | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageCompositor.gd b/src/ImageCompositor.gd index f913a8a..ae47695 100644 --- a/src/ImageCompositor.gd +++ b/src/ImageCompositor.gd @@ -84,6 +84,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 @@ -104,8 +107,5 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message Filesystem.result = get_texture().get_image() image_sprite.texture = ImageTexture.create_from_image(Filesystem.result) image_sprite.material = null - if fit_image: - camera.fit_image() - image_viewport_display.show() # done return errors From 77273f9c1beb6623e4db05042f486a45dff77006 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 31 Mar 2025 15:41:31 +0200 Subject: [PATCH 03/11] Add sobel filter (edge detection) --- examples/sobel.gdshader | 10 +++++++ examples/sobel.gdshader.uid | 1 + shaderlib/sobel.gdshaderinc | 50 +++++++++++++++++++++++++++++++++ shaderlib/sobel.gdshaderinc.uid | 1 + 4 files changed, 62 insertions(+) create mode 100644 examples/sobel.gdshader create mode 100644 examples/sobel.gdshader.uid create mode 100644 shaderlib/sobel.gdshaderinc create mode 100644 shaderlib/sobel.gdshaderinc.uid 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/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 From 560e455cd2aaf37f94a4adfa900c1e83b66451f8 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 31 Mar 2025 21:24:07 +0200 Subject: [PATCH 04/11] Disable VSync while ImageCompositor.update processes the image --- project.godot | 1 - src/Camera.gd | 2 +- src/ImageCompositor.gd | 8 ++++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/project.godot b/project.godot index 918c937..0fb77e0 100644 --- a/project.godot +++ b/project.godot @@ -27,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/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 ae47695..9692fb7 100644 --- a/src/ImageCompositor.gd +++ b/src/ImageCompositor.gd @@ -43,6 +43,12 @@ func shader_has_step_uniform(shader: Shader) -> bool: 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: @@ -98,6 +104,7 @@ 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 for i in range(steps): if has_step_uniform: # set STEP param @@ -106,6 +113,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 # done return errors From bf1c1327ac2e3380cec4a9d1c98d99834baee54e Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Mon, 31 Mar 2025 21:24:43 +0200 Subject: [PATCH 05/11] Add better documentation of value ranges in oklab and oklch --- shaderlib/oklab.gdshaderinc | 6 ++++++ 1 file changed, 6 insertions(+) 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), From 3dcc4ca02c72d0fc1f8c246bce35c61c06e9d0b5 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 1 Apr 2025 21:37:32 +0200 Subject: [PATCH 06/11] shaderlib: Add multipass pixelsorting --- examples/multistep_pixelsort.gdshader | 20 ++++ examples/multistep_pixelsort.gdshader.uid | 1 + shaderlib/pixelsort.gdshaderinc | 126 ++++++++++++++++++++++ shaderlib/pixelsort.gdshaderinc.uid | 1 + 4 files changed, 148 insertions(+) create mode 100644 examples/multistep_pixelsort.gdshader create mode 100644 examples/multistep_pixelsort.gdshader.uid create mode 100644 shaderlib/pixelsort.gdshaderinc create mode 100644 shaderlib/pixelsort.gdshaderinc.uid 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/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 From 4d0f7ca5383e6eb319d055b05cee257ab46584c0 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Tue, 1 Apr 2025 21:42:38 +0200 Subject: [PATCH 07/11] Bump version to v10.1 --- project.godot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.godot b/project.godot index 0fb77e0..4d1b011 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Fragmented" -config/version="v10.0" +config/version="v10.1" run/main_scene="res://src/scenes/main.tscn" config/features=PackedStringArray("4.4", "Mobile") config/icon="res://src/assets/icon.png" From c9c5428dbc0486c7e7a4e3e3fee98b0a01173458 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 9 Apr 2025 19:00:02 +0200 Subject: [PATCH 08/11] shaderlib: Add rotateUV function to common.gdshader --- shaderlib/common.gdshaderinc | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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); +} From ebc8ce2cb9edca0640b6de6ab38f91323b1ed3f0 Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 9 Apr 2025 19:04:11 +0200 Subject: [PATCH 09/11] Add STEPS uniform to get the number of steps programmatically --- src/ImageCompositor.gd | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/ImageCompositor.gd b/src/ImageCompositor.gd index 9692fb7..9f3e95a 100644 --- a/src/ImageCompositor.gd +++ b/src/ImageCompositor.gd @@ -37,9 +37,9 @@ 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 @@ -55,7 +55,8 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message 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!"] @@ -105,6 +106,9 @@ func update(overwrite_image_path: String = "") -> Array: # returns error message 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 From 65fddc0bd8fff83767fafd3fe90f9470bb89afed Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 9 Apr 2025 19:09:32 +0200 Subject: [PATCH 10/11] Update README regarding STEPS uniform --- README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 02aa022..a12517b 100644 --- a/README.md +++ b/README.md @@ -62,15 +62,17 @@ 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 } ``` From 54cbddbbbae169673bb3f44bf588a0f8fc56668e Mon Sep 17 00:00:00 2001 From: ChaoticByte Date: Wed, 9 Apr 2025 19:18:09 +0200 Subject: [PATCH 11/11] Bump version to v10.2 --- project.godot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project.godot b/project.godot index 4d1b011..95da5af 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Fragmented" -config/version="v10.1" +config/version="v10.2" run/main_scene="res://src/scenes/main.tscn" config/features=PackedStringArray("4.4", "Mobile") config/icon="res://src/assets/icon.png"