diff --git a/.gitignore b/.gitignore
index 841dbac..852a042 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,7 +16,6 @@ mono_crash.*.json
*.x86_64
godot.*.template_release.*
dist/*
-!dist/.gitkeep
screenshot.png.import
diff --git a/README.md b/README.md
index c658569..a12517b 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,15 @@

-
Create image filters by writing shaders.
+An image editing/compositing software for graphics programmers.
+
+## Table of Contents
+
+- [Supported Platforms](#supported-platforms)
+- [Usage](#usage)
+- [Shaderlib](#shaderlib)
+- [Commandline interface](#commandline-interface)
+- [Known Issues](#known-issues)
## Supported Platforms
@@ -13,8 +21,16 @@ You can find the latest releases [here](https://github.com/ChaoticByte/Fragmente
## Usage
-The repo includes examples. You can use them as a starting-point to write your own filters.
-Just load an image using `//!load`, edit the shader code and hit `F5` to see the changes.
+With Fragemented, you are processing images with GDShaders. This brings almost endless opportunities to create unique art.
+If you want to learn GDShader, take a look at the [Godot docs](https://docs.godotengine.org/en/stable/tutorials/shaders/).
+
+**The builtin editor got removed** from Fragmented with version **v9.0**. I advise you to write your shaders directly in the Godot Editor.
+
+**To get started, use the project template (see the Releases section of this repo) and open it in Godot.**
+
+The template includes many examples. You can use them as a starting-point to write your own stuff.
+
+Besides the regular GDShader stuff, Fragmented has so-called directives. Those allow to further control the behaviour of the application. **The most important directive is `//!load` to load an image.**
### Load TEXTURE using the `//!load` directive
@@ -46,13 +62,17 @@ Example:
//!load ...
//!steps 5
+uniform int STEP;
+uniform int STEPS;
+
void fragment() {
if (STEP == 0) {
- ...
+ ...
} else if (STEP == 1) {
- ...
+ ...
+ } else if (STEP == STEPS-1) {
+ ...
}
- // ... and so on
}
```
@@ -66,12 +86,15 @@ Here is an example:
```glsl
shader_type canvas_item;
-#include "res://shaderlib/hsv.gdshaderinc"
+#include "./shaderlib/oklab.gdshaderinc"
-//!load ./examples/images/swamp.jpg
+//!load ./images/swamp.jpg
void fragment() {
- COLOR = hsv_offset(COLOR, 0.32, 0.2, 0.0);
+ vec4 oklab = rgb2oklab(COLOR);
+ vec4 oklch = oklab2oklch(oklab);
+ oklch.z -= 2.0;
+ COLOR = oklab2rgb(oklch2oklab(oklch));
}
```
@@ -84,23 +107,50 @@ You can run Fragmented from the commandline or scripts.
### Usage
```
-./Fragmented cmd --shader PATH [--load-image PATH]
+~ Fragmented CLI ~
+-================-
- --shader PATH The path to the shader
- --output PATH Where to write the resulting image to
- --load-image PATH The path to the image. This will overwrite the
- load directive of the shader file (optional)
+Usage:
+
+./Fragmented
+
+Commands:
+
+ help
+
+ | Shows this help text.
+
+ apply --shader PATH [--load-image PATH]
+
+ | Applies a shader file.
+
+ --shader PATH The path to the shader
+ --output PATH Where to write the resulting image to.
+ In batch mode, this must be a folder.
+ --load-image PATH The path to the image. This will overwrite the
+ load directive of the shader file.
+ Passing a folder activates batch mode.
+ (optional)
```
-You can also run `./Fragmented cmd help` to show the help message.
+### Batch Mode
+
+Since version v8.0, you can pass a directory to `--load-image` and `--output`. This will process all images in the input directory and write the output to the output directory.
+
+> Note: You *can* use this feature for video frames, but it will take a loooong time.
#### Examples
```
-./Fragmented cmd --shader ./examples/oklab.gdshader --output ./output.png
+./Fragmented apply --shader ./examples/oklab.gdshader --output ./output.png
```
```
-./Fragmented cmd --shader ./examples/oklab.gdshader --load-image ~/Pictures/test.png --output ./output.png
+./Fragmented apply --shader ./examples/oklab.gdshader --load-image ~/Pictures/test.png --output ./output.png
```
+
+## Known Issues
+
+- screen scaling is unsupported; Using screen scaling could lead to an either blurry UI, or no scaling at all -> see #45
+- commandline interface: `--headless` is not supported
diff --git a/build-template/Containerfile b/build-template/Containerfile
index 38d31a7..b1a0c48 100644
--- a/build-template/Containerfile
+++ b/build-template/Containerfile
@@ -7,15 +7,14 @@ FROM docker.io/ubuntu:focal AS os-base
# https://docs.godotengine.org/en/stable/contributing/development/compiling/compiling_for_linuxbsd.html
RUN apt-get update
-RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq build-essential scons pkg-config libx11-dev libxcursor-dev libxinerama-dev libgl1-mesa-dev libglu1-mesa-dev libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev libwayland-dev
-
-RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq git
+RUN DEBIAN_FRONTEND=noninteractive apt-get install -yq python3-pip git build-essential pkg-config libx11-dev libxcursor-dev libxinerama-dev libgl1-mesa-dev libglu1-mesa-dev libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev libwayland-dev
+RUN pip3 install --system scons
FROM os-base AS clone-src
-RUN git clone https://github.com/godotengine/godot.git -b 4.3-stable /godot-src
+RUN git clone https://github.com/godotengine/godot.git -b 4.4-stable /godot-src
FROM clone-src
WORKDIR /godot-src
-ENTRYPOINT scons platform=linuxbsd target=template_release lto=full optimize=size disable_3d=yes module_text_server_adv_enabled=no module_text_server_fb_enabled=yes module_basis_universal_enabled=no module_csg_enabled=no module_dds_enabled=no module_enet_enabled=no module_gridmap_enabled=no module_hdr_enabled=no module_jsonrpc_enabled=no module_ktx_enabled=no module_mbedtls_enabled=no module_meshoptimizer_enabled=no module_minimp3_enabled=no module_mobile_vr_enabled=no module_msdfgen_enabled=no module_multiplayer_enabled=no module_navigation_enabled=no module_ogg_enabled=no module_openxr_enabled=no module_raycast_enabled=no module_squish_enabled=no module_svg_enabled=no module_tga_enabled=no module_theora_enabled=no module_tinyexr_enabled=no module_upnp_enabled=no module_vhacd_enabled=no module_vorbis_enabled=no module_webrtc_enabled=no module_websocket_enabled=no module_webxr_enabled=no module_zip_enabled=no arch=x86_64 && strip bin/godot.linuxbsd.template_release.x86_64
+ENTRYPOINT scons platform=linuxbsd target=template_release lto=full optimize=size disable_3d=yes module_text_server_adv_enabled=no module_text_server_fb_enabled=yes module_basis_universal_enabled=no module_csg_enabled=no module_enet_enabled=no module_gridmap_enabled=no module_jsonrpc_enabled=no module_mbedtls_enabled=no module_meshoptimizer_enabled=no module_minimp3_enabled=no module_mobile_vr_enabled=no module_msdfgen_enabled=no module_multiplayer_enabled=no module_navigation_enabled=no module_ogg_enabled=no module_openxr_enabled=no module_raycast_enabled=no module_squish_enabled=no module_theora_enabled=no module_upnp_enabled=no module_vhacd_enabled=no module_vorbis_enabled=no module_webrtc_enabled=no module_websocket_enabled=no module_webxr_enabled=no arch=x86_64 && strip bin/godot.linuxbsd.template_release.x86_64
diff --git a/build-template/build.sh b/build-template/build.sh
index 5541b7c..feff902 100755
--- a/build-template/build.sh
+++ b/build-template/build.sh
@@ -3,12 +3,12 @@
set -e
function log {
- echo -e "\033[1;36m* $@\033[0m"
+ echo -e "\033[1;36m***** $@ *****\033[0m"
}
-log
+log " "
log "Fragmented - Godot Build Template Builder"
-log
+log " "
cd $(dirname $0)
log Switched to $(pwd)
diff --git a/dist.sh b/dist.sh
new file mode 100755
index 0000000..3b44438
--- /dev/null
+++ b/dist.sh
@@ -0,0 +1,30 @@
+set -e
+
+function log {
+ echo -e "\033[1;36m***** $@ *****\033[0m"
+}
+
+mkdir -p dist
+
+log Building application
+
+VERSION="$(godot --headless --no-header -s tools/get_version.gd)"
+
+godot --headless --export-release "Linux/X11" "dist/Fragmented-${VERSION}.x86_64"
+
+log Packing shaderlib
+
+ZIP_PATH_SHADERLIB=$(realpath "dist/Fragmented-${VERSION}_shaderlib.zip")
+
+zip -r "${ZIP_PATH_SHADERLIB}" shaderlib/
+
+log Packing project template
+
+ZIP_PATH_PROJECT_TEMPLATE=$(realpath "dist/Fragmented-${VERSION}_project_template.zip")
+
+rm -f "${ZIP_PATH_PROJECT_TEMPLATE}"
+(
+ cd examples/
+ mv project.godot_ project.godot && trap "mv project.godot project.godot_" EXIT
+ zip -r "${ZIP_PATH_PROJECT_TEMPLATE}" *
+)
diff --git a/dist/.gitkeep b/dist/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/examples/0_empty.tscn b/examples/0_empty.tscn
new file mode 100644
index 0000000..5fa71b9
--- /dev/null
+++ b/examples/0_empty.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3 uid="uid://db2rhq8rwv5wo"]
+
+[node name="Node" type="Node"]
diff --git a/examples/blur.gdshader b/examples/blur.gdshader
index 8d08069..fa4d87d 100644
--- a/examples/blur.gdshader
+++ b/examples/blur.gdshader
@@ -1,7 +1,7 @@
shader_type canvas_item;
//!load ./images/swamp.jpg
-#include "res://shaderlib/blur.gdshaderinc"
+#include "./shaderlib/blur.gdshaderinc"
void fragment() {
COLOR = gaussian_blur(TEXTURE, UV, 48, 24.0);
diff --git a/examples/blur.gdshader.uid b/examples/blur.gdshader.uid
new file mode 100644
index 0000000..3739afe
--- /dev/null
+++ b/examples/blur.gdshader.uid
@@ -0,0 +1 @@
+uid://cny8dtukv54wt
diff --git a/examples/channel_offset.gdshader.uid b/examples/channel_offset.gdshader.uid
new file mode 100644
index 0000000..66e7cef
--- /dev/null
+++ b/examples/channel_offset.gdshader.uid
@@ -0,0 +1 @@
+uid://0efk4fornlg6
diff --git a/examples/color_and_pixelate.gdshader b/examples/color_and_pixelate.gdshader
index bf5f18a..f490db4 100644
--- a/examples/color_and_pixelate.gdshader
+++ b/examples/color_and_pixelate.gdshader
@@ -1,7 +1,7 @@
shader_type canvas_item;
-#include "res://shaderlib/colorspaces.gdshaderinc"
-#include "res://shaderlib/effects.gdshaderinc"
+#include "./shaderlib/hsv.gdshaderinc"
+#include "./shaderlib/pixelate.gdshaderinc"
//!load ./images/swamp.jpg
diff --git a/examples/color_and_pixelate.gdshader.uid b/examples/color_and_pixelate.gdshader.uid
new file mode 100644
index 0000000..d943067
--- /dev/null
+++ b/examples/color_and_pixelate.gdshader.uid
@@ -0,0 +1 @@
+uid://gd23hu7ro148
diff --git a/examples/denoise.gdshader b/examples/denoise.gdshader
index 09dabb6..229a8da 100644
--- a/examples/denoise.gdshader
+++ b/examples/denoise.gdshader
@@ -2,7 +2,7 @@ shader_type canvas_item;
//!load ./images/noisy.png
-#include "res://shaderlib/denoise.gdshaderinc"
+#include "./shaderlib/denoise.gdshaderinc"
void fragment() {
COLOR = smart_denoise(TEXTURE, UV, 12.0, 1.0, .12);
diff --git a/examples/denoise.gdshader.uid b/examples/denoise.gdshader.uid
new file mode 100644
index 0000000..9337646
--- /dev/null
+++ b/examples/denoise.gdshader.uid
@@ -0,0 +1 @@
+uid://cbwyneu03fki6
diff --git a/examples/greyscale.gdshader.uid b/examples/greyscale.gdshader.uid
new file mode 100644
index 0000000..b6e1bbd
--- /dev/null
+++ b/examples/greyscale.gdshader.uid
@@ -0,0 +1 @@
+uid://dvarqolt6es27
diff --git a/examples/images/CREDITS.md b/examples/images/CREDITS.md
index ed427c8..d662b8c 100644
--- a/examples/images/CREDITS.md
+++ b/examples/images/CREDITS.md
@@ -3,3 +3,4 @@
- swamp.jpg by [clfr21 on Pixabay](https://pixabay.com/de/users/clfr21-6530007/)
- grass.png by [GDJ on Pixabay](https://pixabay.com/users/gdj-1086657/)
+- mountain.jpg by [Phghvvcftyyufj on Pixabay](https://pixabay.com/users/phghvvcftyyufj-12646982)
diff --git a/examples/images/mountain.jpg b/examples/images/mountain.jpg
new file mode 100644
index 0000000..591c06d
Binary files /dev/null and b/examples/images/mountain.jpg differ
diff --git a/examples/images/mountain.jpg.import b/examples/images/mountain.jpg.import
new file mode 100644
index 0000000..e914676
--- /dev/null
+++ b/examples/images/mountain.jpg.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ben72llmopgaj"
+path="res://.godot/imported/mountain.jpg-c1b7de1e6557b826bc6f9324027e11af.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://examples/images/mountain.jpg"
+dest_files=["res://.godot/imported/mountain.jpg-c1b7de1e6557b826bc6f9324027e11af.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/kuwahara.gdshader b/examples/kuwahara.gdshader
new file mode 100644
index 0000000..246e162
--- /dev/null
+++ b/examples/kuwahara.gdshader
@@ -0,0 +1,16 @@
+shader_type canvas_item;
+
+//!load ./images/mountain.jpg
+
+#include "./shaderlib/kuwahara.gdshaderinc"
+#include "./shaderlib/hsv.gdshaderinc"
+
+void fragment() {
+ // Kuwahara
+ COLOR.rgb = kuwahara(TEXTURE, UV, 20, 80.0, 18.0, 0.6, .15, 8);
+ // A litte bit of color adjustments
+ vec4 hsv = rgb2hsv(COLOR);
+ hsv.x += .03;
+ hsv.y *= 1.4;
+ COLOR = hsv2rgb(hsv);
+}
diff --git a/examples/kuwahara.gdshader.uid b/examples/kuwahara.gdshader.uid
new file mode 100644
index 0000000..2e0241a
--- /dev/null
+++ b/examples/kuwahara.gdshader.uid
@@ -0,0 +1 @@
+uid://cdhyuk7u8kxyk
diff --git a/examples/lowpass.gdshader.uid b/examples/lowpass.gdshader.uid
new file mode 100644
index 0000000..80b19c7
--- /dev/null
+++ b/examples/lowpass.gdshader.uid
@@ -0,0 +1 @@
+uid://dn02xsjm1kok8
diff --git a/examples/multistep_distort.gdshader b/examples/multistep_distort.gdshader
index cea638f..fd7883e 100644
--- a/examples/multistep_distort.gdshader
+++ b/examples/multistep_distort.gdshader
@@ -3,6 +3,8 @@ shader_type canvas_item;
//!steps 9
//!load ./images/swamp.jpg
+uniform int STEP;
+
const float strength = 0.01;
void fragment() {
diff --git a/examples/multistep_distort.gdshader.uid b/examples/multistep_distort.gdshader.uid
new file mode 100644
index 0000000..22efbc6
--- /dev/null
+++ b/examples/multistep_distort.gdshader.uid
@@ -0,0 +1 @@
+uid://c17u5jx7a7o81
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/oklab.gdshader b/examples/oklab.gdshader
index 0d8f093..08f46b5 100644
--- a/examples/oklab.gdshader
+++ b/examples/oklab.gdshader
@@ -1,6 +1,6 @@
shader_type canvas_item;
-#include "res://shaderlib/colorspaces.gdshaderinc"
+#include "./shaderlib/oklab.gdshaderinc"
//!load ./images/swamp.jpg
diff --git a/examples/oklab.gdshader.uid b/examples/oklab.gdshader.uid
new file mode 100644
index 0000000..87cef6b
--- /dev/null
+++ b/examples/oklab.gdshader.uid
@@ -0,0 +1 @@
+uid://cu37y8lc0x83
diff --git a/examples/place_texture.gdshader b/examples/place_texture.gdshader
index 183d080..b0c50ca 100644
--- a/examples/place_texture.gdshader
+++ b/examples/place_texture.gdshader
@@ -1,7 +1,7 @@
shader_type canvas_item;
-#include "res://shaderlib/transform.gdshaderinc"
-#include "res://shaderlib/transparency.gdshaderinc"
+#include "./shaderlib/place_texture.gdshaderinc"
+#include "./shaderlib/common.gdshaderinc"
//!load ./images/swamp.jpg
//!load+ img2 ./images/grass.png
diff --git a/examples/place_texture.gdshader.uid b/examples/place_texture.gdshader.uid
new file mode 100644
index 0000000..8ced27b
--- /dev/null
+++ b/examples/place_texture.gdshader.uid
@@ -0,0 +1 @@
+uid://dybe4t5rbbkc6
diff --git a/examples/project.godot_ b/examples/project.godot_
new file mode 100644
index 0000000..5eff383
--- /dev/null
+++ b/examples/project.godot_
@@ -0,0 +1,15 @@
+; Engine configuration file.
+; It's best edited using the editor UI and not directly,
+; since the parameters that go here are not all obvious.
+;
+; Format:
+; [section] ; section goes between []
+; param=value ; assign values to parameters
+
+config_version=5
+
+[application]
+
+config/name="Fragmented Project"
+run/main_scene="res://0_empty.tscn"
+config/features=PackedStringArray("4.4", "Forward Plus")
diff --git a/examples/shaderlib b/examples/shaderlib
new file mode 120000
index 0000000..dedec01
--- /dev/null
+++ b/examples/shaderlib
@@ -0,0 +1 @@
+../shaderlib
\ No newline at end of file
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 f4ce4eb..79a474d 100644
--- a/export_presets.cfg
+++ b/export_presets.cfg
@@ -8,10 +8,12 @@ dedicated_server=false
custom_features=""
export_filter="all_resources"
include_filter=""
-exclude_filter="screenshot.png, examples/*"
+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 a8dffd7..95da5af 100644
--- a/project.godot
+++ b/project.godot
@@ -11,11 +11,10 @@ config_version=5
[application]
config/name="Fragmented"
-config/version="v7.0"
-run/main_scene="res://scenes/main.tscn"
-config/features=PackedStringArray("4.3", "Mobile")
-run/low_processor_mode=true
-config/icon="res://assets/icon.png"
+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"
[autoload]
@@ -24,11 +23,10 @@ ShaderDirectiveParser="*res://src/ShaderDirectiveParser.gd"
[display]
-window/size/viewport_width=704
-window/size/viewport_height=704
+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]
@@ -72,4 +70,4 @@ renderer/rendering_method="mobile"
textures/vram_compression/import_etc2_astc=true
textures/lossless_compression/force_png=true
shader_compiler/shader_cache/enabled=false
-environment/defaults/default_clear_color=Color(0, 0, 0, 1)
+environment/defaults/default_clear_color=Color(0.501961, 0.501961, 0.501961, 1)
diff --git a/scenes/main.tscn b/scenes/main.tscn
deleted file mode 100644
index 2d104a3..0000000
--- a/scenes/main.tscn
+++ /dev/null
@@ -1,46 +0,0 @@
-[gd_scene load_steps=10 format=3 uid="uid://bjah7k4bxo044"]
-
-[ext_resource type="Script" path="res://src/Main.gd" id="1_2625y"]
-[ext_resource type="Script" path="res://src/ImageCompositor.gd" id="2_4thch"]
-[ext_resource type="Shader" path="res://src/shader/ivd_outline.gdshader" id="3_6xihe"]
-[ext_resource type="Script" path="res://src/ImageViewportDisplay.gd" id="3_n4itb"]
-[ext_resource type="Script" path="res://src/UIWindow.gd" id="6_8k0ha"]
-[ext_resource type="PackedScene" uid="uid://btgits2mfup0h" path="res://scenes/ui_container.tscn" id="7_5ci0e"]
-[ext_resource type="Script" path="res://src/Camera.gd" id="8_mls06"]
-
-[sub_resource type="ShaderMaterial" id="ShaderMaterial_y2ea0"]
-shader = ExtResource("3_6xihe")
-shader_parameter/zoom_level = Vector2(1, 1)
-
-[sub_resource type="ViewportTexture" id="ViewportTexture_lct1c"]
-viewport_path = NodePath("Compositor")
-
-[node name="Main" type="Node2D"]
-script = ExtResource("1_2625y")
-
-[node name="Compositor" type="SubViewport" parent="."]
-unique_name_in_owner = true
-script = ExtResource("2_4thch")
-
-[node name="ImageViewportDisplay" type="Sprite2D" parent="."]
-unique_name_in_owner = true
-material = SubResource("ShaderMaterial_y2ea0")
-texture = SubResource("ViewportTexture_lct1c")
-script = ExtResource("3_n4itb")
-
-[node name="Camera" type="Camera2D" parent="."]
-unique_name_in_owner = true
-script = ExtResource("8_mls06")
-
-[node name="EditorWindow" type="Window" parent="."]
-unique_name_in_owner = true
-disable_3d = true
-position = Vector2i(48, 36)
-size = Vector2i(704, 704)
-visible = false
-script = ExtResource("6_8k0ha")
-
-[node name="UserInterfaceContainer" parent="EditorWindow" instance=ExtResource("7_5ci0e")]
-unique_name_in_owner = true
-
-[connection signal="close_requested" from="EditorWindow" to="EditorWindow" method="_on_close_requested"]
diff --git a/scenes/ui_container.tscn b/scenes/ui_container.tscn
deleted file mode 100644
index 6be4c94..0000000
--- a/scenes/ui_container.tscn
+++ /dev/null
@@ -1,204 +0,0 @@
-[gd_scene load_steps=3 format=3 uid="uid://btgits2mfup0h"]
-
-[ext_resource type="Script" path="res://src/UIAppVersion.gd" id="1_5qvnb"]
-[ext_resource type="Script" path="res://src/Editor.gd" id="2_haub5"]
-
-[node name="UserInterfaceContainer" type="Control"]
-layout_mode = 3
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-grow_horizontal = 2
-grow_vertical = 2
-
-[node name="AppName" type="Label" parent="."]
-layout_mode = 0
-offset_left = 24.0
-offset_top = 16.0
-offset_right = 208.0
-offset_bottom = 48.0
-theme_override_font_sizes/font_size = 20
-text = "Fragmented"
-vertical_alignment = 2
-
-[node name="AppVersion" type="Label" parent="."]
-layout_mode = 0
-offset_left = 152.0
-offset_top = 17.0
-offset_right = 208.0
-offset_bottom = 47.0
-theme_override_font_sizes/font_size = 14
-text = "v0
-"
-vertical_alignment = 2
-script = ExtResource("1_5qvnb")
-
-[node name="Editor" type="Control" parent="."]
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_left = 24.0
-offset_top = 64.0
-offset_right = -24.0
-offset_bottom = -16.0
-grow_horizontal = 2
-grow_vertical = 2
-script = ExtResource("2_haub5")
-
-[node name="OpenShaderDialog" type="FileDialog" parent="Editor"]
-unique_name_in_owner = true
-auto_translate_mode = 1
-title = "Load Shader"
-size = Vector2i(521, 175)
-ok_button_text = "Open"
-mode_overrides_title = false
-file_mode = 0
-access = 2
-filters = PackedStringArray("*.gdshader")
-use_native_dialog = true
-
-[node name="SaveShaderDialog" type="FileDialog" parent="Editor"]
-unique_name_in_owner = true
-auto_translate_mode = 1
-title = "Save Shader"
-size = Vector2i(661, 175)
-ok_button_text = "Save"
-mode_overrides_title = false
-access = 2
-filters = PackedStringArray("*.gdshader")
-use_native_dialog = true
-
-[node name="SaveImageDialog" type="FileDialog" parent="Editor"]
-unique_name_in_owner = true
-auto_translate_mode = 1
-title = "Export Image"
-size = Vector2i(661, 175)
-ok_button_text = "Save"
-mode_overrides_title = false
-access = 2
-filters = PackedStringArray("*.png")
-use_native_dialog = true
-
-[node name="CodeEdit" type="CodeEdit" parent="Editor"]
-unique_name_in_owner = true
-layout_mode = 1
-anchors_preset = 15
-anchor_right = 1.0
-anchor_bottom = 1.0
-offset_top = 48.0
-grow_horizontal = 2
-grow_vertical = 2
-theme_override_font_sizes/font_size = 14
-placeholder_text = "// Test"
-wrap_mode = 1
-minimap_draw = true
-minimap_width = 40
-caret_blink = true
-draw_control_chars = true
-draw_tabs = true
-draw_spaces = true
-line_length_guidelines = Array[int]([80])
-gutters_draw_line_numbers = true
-code_completion_enabled = true
-indent_automatic = true
-auto_brace_completion_enabled = true
-auto_brace_completion_highlight_matching = true
-
-[node name="NewShaderButton" type="Button" parent="Editor"]
-layout_mode = 1
-offset_right = 48.0
-offset_bottom = 32.0
-text = "New"
-
-[node name="OpenShaderButton" type="Button" parent="Editor"]
-layout_mode = 1
-offset_left = 56.0
-offset_right = 112.0
-offset_bottom = 32.0
-text = "Open"
-
-[node name="SaveShaderButton" type="Button" parent="Editor"]
-layout_mode = 1
-offset_left = 120.0
-offset_top = -1.0
-offset_right = 176.0
-offset_bottom = 31.0
-text = "Save"
-
-[node name="SaveShaderAsButton" type="Button" parent="Editor"]
-layout_mode = 1
-offset_left = 184.0
-offset_right = 263.0
-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
-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"]
-layout_mode = 1
-anchors_preset = 1
-anchor_left = 1.0
-anchor_right = 1.0
-offset_left = -128.0
-offset_right = -80.0
-offset_bottom = 32.0
-grow_horizontal = 0
-text = "Fit"
-
-[node name="ApplyShaderButton" type="Button" parent="Editor"]
-layout_mode = 1
-anchors_preset = 1
-anchor_left = 1.0
-anchor_right = 1.0
-offset_left = -232.0
-offset_right = -136.0
-offset_bottom = 31.0
-grow_horizontal = 0
-text = "Apply (F5)"
-
-[node name="StatusIndicator" type="TextureButton" parent="Editor"]
-unique_name_in_owner = true
-layout_mode = 1
-anchors_preset = 1
-anchor_left = 1.0
-anchor_right = 1.0
-offset_left = -76.0
-offset_top = 56.0
-offset_right = -56.0
-offset_bottom = 76.0
-grow_horizontal = 0
-disabled = true
-ignore_texture_size = true
-stretch_mode = 0
-
-[node name="ErrorMessageDialog" type="AcceptDialog" parent="Editor"]
-unique_name_in_owner = true
-title = "Status"
-initial_position = 2
-size = Vector2i(256, 128)
-popup_window = true
-ok_button_text = "Close"
-
-[connection signal="file_selected" from="Editor/OpenShaderDialog" to="Editor" method="_on_open_shader_dialog_file_selected"]
-[connection signal="file_selected" from="Editor/SaveShaderDialog" to="Editor" method="_on_save_shader_dialog_file_selected"]
-[connection signal="file_selected" from="Editor/SaveImageDialog" to="Editor" method="_on_save_image_dialog_file_selected"]
-[connection signal="code_completion_requested" from="Editor/CodeEdit" to="Editor" method="_on_code_edit_code_completion_requested"]
-[connection signal="pressed" from="Editor/NewShaderButton" to="Editor" method="_on_new_shader_button_pressed"]
-[connection signal="pressed" from="Editor/OpenShaderButton" to="Editor" method="_on_open_shader_button_pressed"]
-[connection signal="pressed" from="Editor/SaveShaderButton" to="Editor" method="_on_save_shader_button_pressed"]
-[connection signal="pressed" from="Editor/SaveShaderAsButton" to="Editor" method="_on_save_shader_as_button_pressed"]
-[connection signal="pressed" from="Editor/SaveImageButton" to="Editor" method="_on_save_image_button_pressed"]
-[connection signal="pressed" from="Editor/FitImageButton" to="Editor" method="_on_fit_image_button_pressed"]
-[connection signal="pressed" from="Editor/ApplyShaderButton" to="Editor" method="_on_apply_shader_button_pressed"]
-[connection signal="pressed" from="Editor/StatusIndicator" to="Editor" method="_on_status_indicator_pressed"]
diff --git a/screenshot.png b/screenshot.png
index 19fb490..ce0ac8a 100644
Binary files a/screenshot.png and b/screenshot.png differ
diff --git a/shaderlib/blur.gdshaderinc.uid b/shaderlib/blur.gdshaderinc.uid
new file mode 100644
index 0000000..d5c8dfd
--- /dev/null
+++ b/shaderlib/blur.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://bjtljvcjcu6dr
diff --git a/shaderlib/common.gdshaderinc b/shaderlib/common.gdshaderinc
index 7ee57f8..9352d6a 100644
--- a/shaderlib/common.gdshaderinc
+++ b/shaderlib/common.gdshaderinc
@@ -3,3 +3,25 @@
float cbrt(float x) {
return pow(x, 1.0/3.0);
}
+
+/*
+ Alpha Blending a over b after Bruce A. Wallace
+ source: https://en.wikipedia.org/wiki/Alpha_compositing
+*/
+vec4 alpha_blend(vec4 b, vec4 a) {
+ float alpha = a.a + (b.a * (1.0 - a.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/common.gdshaderinc.uid b/shaderlib/common.gdshaderinc.uid
new file mode 100644
index 0000000..f43430d
--- /dev/null
+++ b/shaderlib/common.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://764b6ekchgb8
diff --git a/shaderlib/denoise.gdshaderinc.uid b/shaderlib/denoise.gdshaderinc.uid
new file mode 100644
index 0000000..132a166
--- /dev/null
+++ b/shaderlib/denoise.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://b7ksfifyyfcip
diff --git a/shaderlib/hsv.gdshaderinc b/shaderlib/hsv.gdshaderinc
new file mode 100644
index 0000000..2a7834d
--- /dev/null
+++ b/shaderlib/hsv.gdshaderinc
@@ -0,0 +1,27 @@
+
+/*
+ rgb2hsv and hsv2rgb functions adapted
+ from https://godotshaders.com/shader/hsv-adjustment/
+ original code by https://godotshaders.com/author/al1-ce/
+
+ Color space conversion functions always work with vec4.
+ The fourth value is always alpha.
+*/
+
+// Convert RGB to HSV (hue, saturation, brightness)
+vec4 rgb2hsv(vec4 c) {
+ vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
+ vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
+ vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
+ float d = q.x - min(q.w, q.y);
+ float e = 1.0e-10;
+ return vec4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
+}
+
+// Convert HSV back to RGB (red, green, blue)
+vec4 hsv2rgb(vec4 c) {
+ vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
+ vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
+ vec3 rgb = c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
+ return vec4(rgb.r, rgb.g, rgb.b, c.a);
+}
diff --git a/shaderlib/hsv.gdshaderinc.uid b/shaderlib/hsv.gdshaderinc.uid
new file mode 100644
index 0000000..771cc38
--- /dev/null
+++ b/shaderlib/hsv.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://bbr3tq6mp5qa2
diff --git a/shaderlib/kuwahara.gdshaderinc b/shaderlib/kuwahara.gdshaderinc
new file mode 100644
index 0000000..ea6e22b
--- /dev/null
+++ b/shaderlib/kuwahara.gdshaderinc
@@ -0,0 +1,116 @@
+/*
+ Kuwahara Filter, adapted
+ original code: https://godotshaders.com/shader/generalized-kuwahara/
+ original authors:
+ - https://godotshaders.com/author/firerabbit/
+ - https://github.com/GarrettGunnell (Acerola)
+ license of the original code:
+
+ MIT License
+
+ Copyright (c) 2022 Garrett Gunnell
+ Copyright (c) 2024 Firerabbit
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this
+ software and associated documentation files (the "Software"), to deal in the Software
+ without restriction, including without limitation the rights to use, copy, modify, merge,
+ publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+ to whom the Software is furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all copies
+ or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ IN THE SOFTWARE.
+*/
+
+vec3 kuwahara(
+ sampler2D texture,
+ vec2 uv,
+ int kernel_size, // should be > 2 - high values will affect performance
+ float hardness, // should be in the range of 1.0 - 100.0
+ float sharpness, // should be in the range of 1.0 - 18.0
+ float zero_crossing, // should be in the range of 0.5 - 2.0
+ float zeta, // should be in the range of 0.01 - 3.0
+ int n // number of iterations, should be 8, must be <= 8
+) {
+ vec2 texelSize = vec2(1.0 / vec2(textureSize(texture, 0)));
+ vec4 m[8];
+ vec3 s[8];
+
+ int kernel_radius = kernel_size / 2;
+
+ float sin_zero_crossing = sin(zero_crossing);
+ float eta = (zeta + cos(zero_crossing)) / (sin_zero_crossing * sin_zero_crossing);
+
+ for (int k = 0; k < n; ++k) {
+ m[k] = vec4(0.0f);
+ s[k] = vec3(0.0f);
+ }
+
+ for (int y = -kernel_radius; y <= kernel_radius; ++y) {
+ for (int x = -kernel_radius; x <= kernel_radius; ++x) {
+ vec2 v = vec2(float(x), float(y)) / float(kernel_radius);
+ vec3 c = texture(texture, uv + vec2(float(x), float(y)) * texelSize.xy).rgb;
+ c = clamp(c, 0.0f, 1.0f);
+ float sum = 0.0f;
+ float w[8];
+ float z, vxx, vyy;
+
+ /* Calculate Polynomial Weights */
+ vxx = zeta - eta * v.x * v.x;
+ vyy = zeta - eta * v.y * v.y;
+ z = max(0, v.y + vxx);
+ w[0] = z * z;
+ sum += w[0];
+ z = max(0, -v.x + vyy);
+ w[2] = z * z;
+ sum += w[2];
+ z = max(0, -v.y + vxx);
+ w[4] = z * z;
+ sum += w[4];
+ z = max(0, v.x + vyy);
+ w[6] = z * z;
+ sum += w[6];
+ v = sqrt(2.0f) / 2.0f * vec2(v.x - v.y, v.x + v.y);
+ vxx = zeta - eta * v.x * v.x;
+ vyy = zeta - eta * v.y * v.y;
+ z = max(0, v.y + vxx);
+ w[1] = z * z;
+ sum += w[1];
+ z = max(0, -v.x + vyy);
+ w[3] = z * z;
+ sum += w[3];
+ z = max(0, -v.y + vxx);
+ w[5] = z * z;
+ sum += w[5];
+ z = max(0, v.x + vyy);
+ w[7] = z * z;
+ sum += w[7];
+
+ float g = exp(-3.125f * dot(v,v)) / sum;
+
+ for (int k = 0; k < 8; ++k) {
+ float wk = w[k] * g;
+ m[k] += vec4(c * wk, wk);
+ s[k] += c * c * wk;
+ }
+ }
+ }
+
+ vec4 output = vec4(0.0f);
+ for (int k = 0; k < n; ++k) {
+ m[k].rgb /= m[k].w;
+ s[k] = abs(s[k] / m[k].w - m[k].rgb * m[k].rgb);
+ float sigma2 = s[k].r + s[k].g + s[k].b;
+ float w = 1.0f / (1.0f + pow(hardness * 1000.0f * sigma2, 0.5f * sharpness));
+ output += vec4(m[k].rgb * w, w);
+ }
+
+ return clamp(output / output.w, 0.0f, 1.0f).rgb;
+}
diff --git a/shaderlib/kuwahara.gdshaderinc.uid b/shaderlib/kuwahara.gdshaderinc.uid
new file mode 100644
index 0000000..1ff911b
--- /dev/null
+++ b/shaderlib/kuwahara.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://chqh2cni1qiuu
diff --git a/shaderlib/colorspaces.gdshaderinc b/shaderlib/oklab.gdshaderinc
similarity index 62%
rename from shaderlib/colorspaces.gdshaderinc
rename to shaderlib/oklab.gdshaderinc
index 6a8ae9b..5a456c7 100644
--- a/shaderlib/colorspaces.gdshaderinc
+++ b/shaderlib/oklab.gdshaderinc
@@ -1,43 +1,19 @@
-/*
- Color space conversion functions always work with vec4.
- The fourth value is always alpha.
-*/
-
-#include "res://shaderlib/common.gdshaderinc"
-
-/*
- rgb2hsv and hsv2rgb functions adapted
- from https://godotshaders.com/shader/hsv-adjustment/
- original code by https://godotshaders.com/author/al1-ce/
-*/
-
-// Convert RGB to HSV (hue, saturation, brightness)
-vec4 rgb2hsv(vec4 c) {
- vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
- vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
- vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
- float d = q.x - min(q.w, q.y);
- float e = 1.0e-10;
- return vec4(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x, c.a);
-}
-
-// Convert HSV back to RGB (red, green, blue)
-vec4 hsv2rgb(vec4 c) {
- vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
- vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
- vec3 rgb = c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
- return vec4(rgb.r, rgb.g, rgb.b, c.a);
-}
-
/*
OkLab and OkLCh
For more details on oklab, see
- https://bottosson.github.io/posts/oklab/
- https://en.wikipedia.org/wiki/Oklab_color_space
+
+ Color space conversion functions always work with vec4.
+ The fourth value is always alpha.
*/
+#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;
@@ -55,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;
@@ -72,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)),
@@ -81,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/oklab.gdshaderinc.uid b/shaderlib/oklab.gdshaderinc.uid
new file mode 100644
index 0000000..7f0bf5f
--- /dev/null
+++ b/shaderlib/oklab.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://ckw4nfslk4m6l
diff --git a/shaderlib/effects.gdshaderinc b/shaderlib/pixelate.gdshaderinc
similarity index 100%
rename from shaderlib/effects.gdshaderinc
rename to shaderlib/pixelate.gdshaderinc
diff --git a/shaderlib/pixelate.gdshaderinc.uid b/shaderlib/pixelate.gdshaderinc.uid
new file mode 100644
index 0000000..a0a0d4d
--- /dev/null
+++ b/shaderlib/pixelate.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://dpu5nneo5bgnq
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/transform.gdshaderinc b/shaderlib/place_texture.gdshaderinc
similarity index 100%
rename from shaderlib/transform.gdshaderinc
rename to shaderlib/place_texture.gdshaderinc
diff --git a/shaderlib/place_texture.gdshaderinc.uid b/shaderlib/place_texture.gdshaderinc.uid
new file mode 100644
index 0000000..f0b6f69
--- /dev/null
+++ b/shaderlib/place_texture.gdshaderinc.uid
@@ -0,0 +1 @@
+uid://51u2hjq62e5i
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/shaderlib/transparency.gdshaderinc b/shaderlib/transparency.gdshaderinc
deleted file mode 100644
index a97e4b7..0000000
--- a/shaderlib/transparency.gdshaderinc
+++ /dev/null
@@ -1,10 +0,0 @@
-
-/*
- Alpha Blending a over b after Bruce A. Wallace
- source: https://en.wikipedia.org/wiki/Alpha_compositing
-*/
-vec4 alpha_blend(vec4 b, vec4 a) {
- float alpha = a.a + (b.a * (1.0 - a.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);
-}
diff --git a/src/Camera.gd b/src/Camera.gd
index 5ad2855..416e035 100644
--- a/src/Camera.gd
+++ b/src/Camera.gd
@@ -1,9 +1,9 @@
extends Camera2D
-var drag = false
-
@onready var image_viewport_display = %ImageViewportDisplay
+var drag = false
+
func _input(event):
if event.is_action_pressed("zoom_out"):
zoom_out()
@@ -16,31 +16,35 @@ func _input(event):
if self.drag && event is InputEventMouseMotion:
self.global_position -= event.relative / self.zoom
+var old_zoom = self.zoom
+
+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)
+ old_zoom = self.zoom
+
func fit_image():
if Filesystem.original_image != null:
var image_size = Filesystem.original_image.get_size()
var viewport_size = get_viewport_rect().size
- var zoomf = viewport_size.x / image_size.x / 1.1
- if zoomf * image_size.y > viewport_size.y:
- zoomf = viewport_size.y / image_size.y / 1.1
+ var zoomf = 1.0
+ if viewport_size.x / image_size.x * image_size.y > viewport_size.y:
+ zoomf = viewport_size.y / image_size.y / 1.25
+ else:
+ zoomf = viewport_size.x / image_size.x / 1.2
self.zoom = Vector2(zoomf, zoomf)
self.global_position = Vector2(0, 0)
-func update_vd_zoomlevel():
- image_viewport_display.update_zoom_texture_filter(self.zoom)
- image_viewport_display.material.set_shader_parameter("zoom_level", self.zoom)
-
func zoom_in():
var old_mouse_pos = get_global_mouse_position()
self.zoom *= 1.2
self.global_position += old_mouse_pos - get_global_mouse_position()
- update_vd_zoomlevel()
func zoom_out():
var old_mouse_pos = get_global_mouse_position()
self.zoom *= 1/1.2
self.global_position += old_mouse_pos - get_global_mouse_position()
- update_vd_zoomlevel()
func _on_fit_image_button_pressed():
fit_image()
diff --git a/src/Camera.gd.uid b/src/Camera.gd.uid
new file mode 100644
index 0000000..f25b715
--- /dev/null
+++ b/src/Camera.gd.uid
@@ -0,0 +1 @@
+uid://b6r8rigubdctk
diff --git a/src/Editor.gd b/src/Editor.gd
deleted file mode 100644
index 4fb716c..0000000
--- a/src/Editor.gd
+++ /dev/null
@@ -1,307 +0,0 @@
-extends Control
-
-@onready var code_editor = %CodeEdit
-
-@onready var open_shader_dialog = %OpenShaderDialog
-@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
-
-@onready var main = get_tree().root.get_node("Main")
-@onready var compositor = main.get_node("%Compositor")
-@onready var camera = main.get_node("%Camera")
-
-#
-
-var status_okay_texture: CompressedTexture2D = preload("uid://m1omb6g45vst")
-var status_error_texture: CompressedTexture2D = preload("uid://04iv1gogpuhu")
-
-# # # # # # # # # # #
-# GDShader keywords #
-# https://github.com/godotengine/godot/blob/e96ad5af98547df71b50c4c4695ac348638113e0/servers/rendering/shader_language.cpp
-# https://github.com/godotengine/godot/blob/e96ad5af98547df71b50c4c4695ac348638113e0/servers/rendering/shader_types.cpp
-#
-const gdshader_boolean_values = [
- "true", "false"
-]
-const gdshader_datatypes = [
- "void",
- "bool", "bvec2", "bvec3", "bvec4",
- "int", "ivec2", "ivec3", "ivec4",
- "uint", "uvec2", "uvec3", "uvec4",
- "float", "vec2", "vec3", "vec4",
- "mat2", "mat3", "mat4",
- "sampler2D", "isampler2D", "usampler2D",
- "sampler2DArray", "isampler2DArray", "usampler2DArray",
-]
-const gdshader_precision_modifiers = [
- "lowp", "mediump", "heighp"
-]
-const gdshader_global_space_keywords = [
- "uniform", "group_uniforms", "varying", "const",
- "struct", "shader_type", "render_mode"
-]
-const gdshader_uniform_qualifiers = [
- "instance", "global"
-]
-const gdshader_block_keywords = [
- "if", "else",
- "for", "while", "do",
- "switch", "case",
- "default", "break", "continue",
- "return", "discard"
-]
-const gdshader_function_specifier_keywords = [
- "in", "out", "inout"
-]
-const gdshader_hints = [
- "source_color", "hint_range", "instance_index"
-]
-const gdshader_sampler_hints = [
- "hint_normal",
- "hint_default_white", "hint_default_black", "hint_default_transparent",
- "hint_anisotropy",
- "hint_roughness_r", "hint_roughness_g", "hint_roughness_b", "hint_roughness_a",
- "hint_roughness_normal", "hint_roughness_gray",
- "hint_screen_texture", "hint_normal_roughness_texture",
- "hint_depth_texture",
- "filter_nearest", "filter_linear",
- "filter_nearest_mipmap", "filter_linear_mipmap",
- "filter_nearest_mipmap_anisotropic", "filter_linear_mipmap_anisotropic",
- "repeat_enable", "repeat_disable"
-]
-const gdshader_builtin_functions = [
- "radians", "degrees",
- "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh",
- "asinh", "acosh", "atanh",
- "pow", "exp", "exp2", "log", "log2", "sqrt", "inversesqrt",
- "abs", "sign", "floor", "trunc", "round", "roundEven", "ceil", "fract",
- "mod", "modf", "min", "max", "clamp",
- "mix", "step", "smoothstep",
- "isnan", "isinf",
- "floatBitsToInt", "floatBitsToUint", "intBitsToFloat", "uintBitsToFloat",
- "length", "distance",
- "dot", "cross",
- "normalize", "reflect", "refract", "faceforward",
- "matrixCompMult", "outerProduct", "transpose",
- "determinant", "inverse",
- "lessThan", "greaterThan", "lessThanEqual", "greaterThanEqual",
- "equal", "notEqual",
- "any", "all", "not",
- "textureSize", "texture", "textureProj", "textureLod", "texelFetch",
- "textureProjLod", "textureGrad", "textureProjGrad", "textureGather",
- "textureQueryLod", "textureQueryLevels",
- "dFdx", "dFdxCoarse", "dFdxFine", "dFdy", "dFdyCoarse", "dFdyFine",
- "fwidth", "fwidthCoarse", "fwidthFine"
-]
-const gdshader_sub_functions = [
- "length", "fma",
- "packHalf2x16", "packUnorm2x16", "packSnorm2x16", "packUnorm4x8", "packSnorm4x8",
- "unpackHalf2x16", "unpackUnorm2x16", "unpackSnorm2x16", "unpackUnorm4x8", "unpackSnorm4x8",
- "bitfieldExtract", "bitfieldInsert", "bitfieldReverse", "bitCount",
- "findLSB", "findMSB",
- "umulExtended", "imulExtended",
- "uaddCarry", "usubBorrow",
- "ldexp", "frexp"
-]
-const gdshader_builtins = [
- "TIME", "PI", "TAU", "E",
- "VERTEX",
- "UV",
- "COLOR",
- "POINT_SIZE",
- "AT_LIGHT_PASS",
- "TEXTURE_PIXEL_SIZE",
- "SHADOW_VERTEX", "LIGHT_VERTEX",
- "FRAGCOORD",
- "NORMAL", "NORMAL_MAP", "NORMAL_MAP_DEPTH",
- "TEXTURE",
- "POINT_COORD",
- "SPECULAR_SHININESS"
-]
-const gdshader_preprocessor = [
- "define", "undef", "include", "pragma",
- "if", "elif", "ifdef", "ifndef", "else", "endif"
-]
-# shaderlib
-var shaderlib_regex = {
- "colorspaces": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/colorspaces\.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\"'),
- "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\"'),
- "blur": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/blur\.gdshaderinc\"'),
-}
-const shaderlib_functions = {
- "colorspaces": ["rgb2hsv", "hsv2rgb", "oklab2rgb", "rgb2oklab", "oklab2oklch", "oklch2oklab"],
- "transform": ["place_texture"],
- "transparency": ["alpha_blend"],
- "effects": ["pixelate"],
- "denoise": ["smart_denoise"],
- "blur": ["gaussian_blur"],
-}
-#
-# configure Highlighter
-#
-class ShaderSyntaxHighlighter extends CodeHighlighter:
- func _init():
- add_color_region("//", "", Color.WEB_GRAY, true)
- add_color_region("/*", "*/", Color.WEB_GRAY, false)
- function_color = Color.INDIAN_RED
- for k in gdshader_boolean_values:
- keyword_colors[k] = Color.INDIAN_RED
- for k in ( gdshader_datatypes
- + gdshader_hints
- + gdshader_sampler_hints
- + gdshader_global_space_keywords
- + gdshader_function_specifier_keywords
- + gdshader_precision_modifiers
- + gdshader_uniform_qualifiers):
- keyword_colors[k] = Color.ORCHID;
- for k in gdshader_block_keywords:
- keyword_colors[k] = Color.CORAL
- for k in gdshader_builtins:
- keyword_colors[k] = Color.DARK_TURQUOISE
- member_variable_color = Color.LIGHT_BLUE
- number_color = Color.AQUA
- symbol_color = Color.GRAY
-#
-# and code completion
-#
-func _on_code_edit_code_completion_requested():
- for k in gdshader_boolean_values:
- code_editor.code_completion_prefixes.append(k)
- code_editor.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, k, k, Color.INDIAN_RED)
- for k in ( gdshader_datatypes
- + gdshader_hints
- + gdshader_sampler_hints
- + gdshader_global_space_keywords
- + gdshader_function_specifier_keywords
- + gdshader_precision_modifiers
- + gdshader_uniform_qualifiers):
- code_editor.code_completion_prefixes.append(k)
- code_editor.add_code_completion_option(CodeEdit.KIND_CLASS, k, k, Color.ORCHID)
- for k in gdshader_block_keywords:
- code_editor.code_completion_prefixes.append(k)
- code_editor.add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, k, k, Color.CORAL)
- for k in gdshader_builtins:
- code_editor.code_completion_prefixes.append(k)
- code_editor.add_code_completion_option(CodeEdit.KIND_CONSTANT, k, k, Color.DARK_TURQUOISE)
- 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:
- if shaderlib_regex[key].search(shader_code) != null:
- if key in shaderlib_functions:
- for k in shaderlib_functions[key]:
- code_editor.code_completion_prefixes.append(k)
- code_editor.add_code_completion_option(CodeEdit.KIND_FUNCTION, k, k+"(", Color.INDIAN_RED)
- # # # # # # #
- code_editor.update_code_completion_options(true)
-#
-# # # # # # # # # # # #
-
-func _ready():
- code_editor.code_completion_enabled = true
- code_editor.syntax_highlighter = ShaderSyntaxHighlighter.new()
- self.update_code_edit()
-
-func _input(event):
- if event.is_action_pressed("apply_shader"):
- _on_apply_shader_button_pressed()
- elif event.is_action_pressed("save_shader"):
- accept_event() # Event is now handled.
- _on_save_shader_button_pressed()
-
-func update_code_edit():
- code_editor.text = Filesystem.shader_code
-
-enum Status {OKAY, ERROR, UNKNOWN = -1}
-
-func update_status(status: Status, msg: String = ""):
- error_msg_dialog.dialog_text = msg
- error_msg_dialog.reset_size()
- if status == Status.OKAY:
- status_indicator.texture_normal = status_okay_texture
- elif status == Status.ERROR:
- status_indicator.texture_normal = status_error_texture
- else:
- status_indicator.texture_normal = null
- if msg == "":
- status_indicator.disabled = true
- else:
- status_indicator.disabled = false
-
-#
-
-func _on_new_shader_button_pressed():
- main.update_title()
- Filesystem.reset()
- self.update_code_edit()
- compositor.update()
- update_status(Status.UNKNOWN)
-
-func _on_open_shader_button_pressed():
- open_shader_dialog.show()
-
-func _on_save_shader_button_pressed():
- Filesystem.shader_code = code_editor.text
- if Filesystem.last_shader_savepath == "":
- _on_save_shader_as_button_pressed()
- else:
- _on_save_shader_dialog_file_selected(Filesystem.last_shader_savepath)
-
-func _on_save_shader_as_button_pressed() -> void:
- Filesystem.shader_code = code_editor.text
- if Filesystem.last_shader_savepath == "":
- save_shader_dialog.current_file = "filter.gdshader"
- else:
- save_shader_dialog.current_path = Filesystem.last_shader_savepath
- save_shader_dialog.show()
-
-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:
- ui_control_filesave.current_path = Filesystem.last_image_savepath
- ui_control_filesave.show()
-
-#
-
-func _on_open_shader_dialog_file_selected(path: String):
- Filesystem.load_shader(path)
- main.update_title(path.split("/")[-1])
- self.update_code_edit()
- self._on_apply_shader_button_pressed()
-
-func _on_save_shader_dialog_file_selected(path):
- Filesystem.save_shader(path)
- main.update_title(path.split("/")[-1])
-
-func _on_save_image_dialog_file_selected(path):
- Filesystem.save_result(path)
-
-#
-
-func _on_status_indicator_pressed() -> void:
- error_msg_dialog.show()
diff --git a/src/Filesystem.gd b/src/Filesystem.gd
index 0fb9507..cb1ac7d 100644
--- a/src/Filesystem.gd
+++ b/src/Filesystem.gd
@@ -1,25 +1,31 @@
extends Node
-@onready var template_shader: Shader = load("res://src/shader/template.gdshader")
-@onready var shader_code: String = template_shader.code
+var cwd = "."
+
+var shader_path = "":
+ get():
+ return shader_path
+ set(v):
+ var old = shader_path
+ shader_path = v
+ if "/" in v: # update current working directory
+ cwd = v.substr(0, v.rfind("/"))
+ if old != shader_path:
+ store_last_opened_file()
+
+var shader: Shader:
+ get():
+ print("Load ", shader_path)
+ return load(shader_path)
var original_image: ImageTexture
+
var additional_images: Dictionary
var result: Image
-var cwd = "."
var last_image_savepath = ""
-var last_shader_savepath = ""
var last_original_image_path = ""
-func reset():
- self.shader_code = self.template_shader.code
- self.last_image_savepath = ""
- self.last_shader_savepath = ""
- self.last_original_image_path = ""
- self.original_image = null
- self.result = null
-
func get_absolute_path(p: String) -> String:
# this only works on Linux!
if !p.begins_with("/"):
@@ -27,14 +33,14 @@ func get_absolute_path(p: String) -> String:
return p
func load_original_image(path: String) -> String: # returns an error message
+ print("Load ", path)
var img = Image.new()
var err = img.load(path)
if err == OK:
original_image = ImageTexture.create_from_image(img)
- if path != self.last_original_image_path:
- self.last_original_image_path = path
- if self.last_image_savepath == "":
+ if self.last_image_savepath == "" or path != self.last_original_image_path:
self.last_image_savepath = path
+ self.last_original_image_path = path
return ""
return error_string(err) + " " + path
@@ -42,6 +48,7 @@ func clear_additional_images():
additional_images.clear()
func load_additional_image(key: String, path: String) -> String: # returns Error Message String
+ print("Load ", path)
var img = Image.new()
var err = img.load(path)
if err == OK:
@@ -58,33 +65,13 @@ func save_result(path: String):
else:
self.last_image_savepath = path
-func load_shader(path: String):
- print("Load ", path)
- var file = FileAccess.open(path, FileAccess.READ)
- 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.store_pascal_string(shader_path)
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()
+ shader_path = f.get_pascal_string()
diff --git a/src/Filesystem.gd.uid b/src/Filesystem.gd.uid
new file mode 100644
index 0000000..a89dcab
--- /dev/null
+++ b/src/Filesystem.gd.uid
@@ -0,0 +1 @@
+uid://rlb041ygdwol
diff --git a/src/ImageCompositor.gd b/src/ImageCompositor.gd
index 5999b88..9f3e95a 100644
--- a/src/ImageCompositor.gd
+++ b/src/ImageCompositor.gd
@@ -37,17 +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 inject_step_uniform(shader_code: String) -> Shader:
- var shader = Shader.new()
- # this should run after validate_shader_compilation()
- var fragment_function_match = _fragment_function_regex.search(shader_code)
- shader.code = shader_code.insert(fragment_function_match.get_start(), "\nuniform int STEP;")
- return shader
+func shader_has_uniform(shader: Shader, name: String, type: int) -> bool:
+ for u in shader.get_shader_uniform_list():
+ 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)
- # inject STEP uniform & get number of steps
- var shader: Shader = inject_step_uniform(Filesystem.shader_code)
+ 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_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!"]
@@ -82,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
@@ -93,17 +105,19 @@ 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):
- # set STEP param
- mat.set_shader_parameter("STEP", i)
+ if has_step_uniform:
+ # set STEP param
+ mat.set_shader_parameter("STEP", i)
# Get viewport texture
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()
- camera.update_vd_zoomlevel()
- image_viewport_display.show()
# done
return errors
diff --git a/src/ImageCompositor.gd.uid b/src/ImageCompositor.gd.uid
new file mode 100644
index 0000000..2e55fae
--- /dev/null
+++ b/src/ImageCompositor.gd.uid
@@ -0,0 +1 @@
+uid://d106170kuigl3
diff --git a/src/ImageViewportDisplay.gd.uid b/src/ImageViewportDisplay.gd.uid
new file mode 100644
index 0000000..dbde761
--- /dev/null
+++ b/src/ImageViewportDisplay.gd.uid
@@ -0,0 +1 @@
+uid://ctc4lhbdsoq7u
diff --git a/src/Main.gd b/src/Main.gd
index 8e54a86..dcf32b3 100644
--- a/src/Main.gd
+++ b/src/Main.gd
@@ -1,17 +1,27 @@
extends Node
-@onready var editor_window = %EditorWindow
-@onready var ui_container = %UserInterfaceContainer
+const BATCH_MODE_SUPPORTED_EXTS = [
+ ".bmp", ".dds", ".exr", ".hdr", ".jpeg", ".jpg", ".ktx", ".png", ".svg", ".webp"
+]
+
@onready var app_name = ProjectSettings.get_setting("application/config/name")
func show_help():
print(
"Usage:\n\n",
- "./Fragmented cmd --shader PATH [--load-image PATH]\n\n",
- " --shader PATH The path to the shader\n",
- " --output PATH Where to write the resulting image to\n",
- " --load-image PATH The path to the image. This will overwrite the\n",
- " load directive of the shader file (optional)\n")
+ "./Fragmented \n\n",
+ "Commands:\n\n",
+ " help\n\n",
+ " | Shows this help text.\n\n",
+ " apply --shader PATH [--load-image PATH]\n\n",
+ " | Applies a shader file.\n\n",
+ " --shader PATH The path to the shader\n",
+ " --output PATH Where to write the resulting image to.\n",
+ " In batch mode, this must be a folder.\n",
+ " --load-image PATH The path to the image. This will overwrite the\n",
+ " load directive of the shader file.\n",
+ " Passing a folder activates batch mode.\n",
+ " (optional)\n")
func parse_custom_cmdline(args: PackedStringArray):
var kwargs: Dictionary = {"--shader": null, "--output": null, "--load-image": null}
@@ -25,50 +35,92 @@ func parse_custom_cmdline(args: PackedStringArray):
i += 1
return kwargs
-func _ready():
- var args = OS.get_cmdline_args()
- if "cmd" in args: # commandline interface
- if "help" in args:
- show_help()
- get_tree().quit(1)
- else:
- var kwargs: Dictionary = parse_custom_cmdline(args)
- if kwargs["--shader"] == null or kwargs["--output"] == null:
+func cli_handle_errors(errors: Array) -> int:
+ # returns number of errors
+ var n_errors = errors.size()
+ if n_errors > 0:
+ print("One or more errors occurred.")
+ for e in errors:
+ printerr(e)
+ return n_errors
+
+func cli(args: PackedStringArray):
+ print(
+ "~ Fragmented CLI ~\n",
+ "-================-\n")
+ if "help" in args:
+ show_help()
+ get_tree().quit(1)
+ return
+ var kwargs: Dictionary = parse_custom_cmdline(args)
+ if kwargs["--shader"] == null or kwargs["--output"] == null:
+ show_help()
+ get_tree().quit(1)
+ return
+ var batch_mode = false
+ var load_image_dir: DirAccess
+ if kwargs["--load-image"] != null:
+ load_image_dir = DirAccess.open(kwargs["--load-image"])
+ if load_image_dir != null:
+ # batch mode
+ if DirAccess.open(kwargs["--output"]) == null:
+ printerr("If --load-image is a directory, --output has to be one too.\n")
show_help()
get_tree().quit(1)
+ return
else:
- Filesystem.load_shader(kwargs["--shader"])
- var errors = []
- if kwargs["--load-image"] == null:
- errors = await $Compositor.update()
+ batch_mode = true
+ #
+ Filesystem.shader_path = kwargs["--shader"]
+ #
+ if batch_mode:
+ var in_dir_path = load_image_dir.get_current_dir()
+ var out_dir_path: String = kwargs["--output"].rstrip("/")
+ for f in load_image_dir.get_files():
+ var supported = false
+ for e in BATCH_MODE_SUPPORTED_EXTS:
+ if f.ends_with(e):
+ supported = true
+ break
+ if supported:
+ f = in_dir_path + "/" + f
+ print(f)
+ var errors = await $Compositor.update(f)
+ if cli_handle_errors(errors) == 0:
+ var filename = out_dir_path + "/" + f.substr(f.rfind("/"), -1)
+ Filesystem.save_result(filename)
else:
- errors = await $Compositor.update(kwargs["--load-image"])
- if errors.size() > 0:
- print("One or more errors occurred.")
- for e in errors:
- printerr(e)
get_tree().quit(1)
- else:
- Filesystem.save_result(kwargs["--output"])
- get_tree().quit(0)
+ return
+ get_tree().quit(0)
else:
- update_title()
- # position windows
- get_window().position = Vector2i(
- editor_window.position.x + editor_window.size.x + 50,
- editor_window.position.y)
- get_window().min_size = Vector2i(400, 400)
- editor_window.min_size = Vector2i(560, 400)
- editor_window.show()
- # 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)
+ var errors = []
+ if kwargs["--load-image"] == null:
+ errors = await $Compositor.update()
+ else:
+ errors = await $Compositor.update(kwargs["--load-image"])
+ if cli_handle_errors(errors) == 0:
+ Filesystem.save_result(kwargs["--output"])
+ get_tree().quit(0)
+ else:
+ get_tree().quit(1)
+
+func prepare_gui():
+ update_title()
+ # Load last opened file
+ Filesystem.remember_last_opened_file()
+ %MainUI._on_apply_shader_button_pressed()
+
+func _ready():
+ var args = OS.get_cmdline_args()
+ if len(args) > 0 and args[0] in ["apply", "help"]:
+ # use the commandline interface
+ cli(args)
+ else:
+ prepare_gui()
func update_title(current_file: String = ""):
if current_file == "":
get_window().title = app_name + " - Viewer"
- editor_window.title = app_name + " - Editor"
else:
get_window().title = current_file + " - " + app_name + " - Viewer"
- editor_window.title = current_file + " - " + app_name + " - Editor"
diff --git a/src/Main.gd.uid b/src/Main.gd.uid
new file mode 100644
index 0000000..2f9a508
--- /dev/null
+++ b/src/Main.gd.uid
@@ -0,0 +1 @@
+uid://5sbslwysin5a
diff --git a/src/MainUI.gd b/src/MainUI.gd
new file mode 100644
index 0000000..8a62ff1
--- /dev/null
+++ b/src/MainUI.gd
@@ -0,0 +1,100 @@
+extends Control
+
+@onready var open_shader_dialog = %OpenShaderDialog
+@onready var save_image_dialog = %SaveImageDialog
+
+@onready var open_shader_button = %OpenShaderButton
+@onready var save_image_button = %SaveImageButton
+@onready var fit_image_button = %FitImageButton
+@onready var apply_shader_button = %ApplyShaderButton
+
+@onready var status_indicator = %StatusIndicator
+@onready var error_msg_dialog = %ErrorMessageDialog
+
+@onready var main = get_tree().root.get_node("Main")
+@onready var compositor = %Compositor
+@onready var camera = %Camera
+
+var status_okay_texture: CompressedTexture2D = preload("uid://m1omb6g45vst")
+var status_error_texture: CompressedTexture2D = preload("uid://04iv1gogpuhu")
+
+enum Status {OKAY, ERROR, UNKNOWN = -1}
+
+#
+
+func _input(event):
+ if event.is_action_pressed("apply_shader"):
+ _on_apply_shader_button_pressed()
+ elif event.is_action_pressed("save_shader"):
+ accept_event() # Event is now handled.
+
+#
+
+func set_buttons_disabled(disabled: bool):
+ for b in [open_shader_button, save_image_button, fit_image_button, apply_shader_button, status_indicator]:
+ b.disabled = disabled
+
+func _on_open_shader_button_pressed():
+ set_buttons_disabled(true)
+ open_shader_dialog.show()
+
+func _on_fit_image_button_pressed():
+ camera.fit_image()
+
+func _on_apply_shader_button_pressed():
+ set_buttons_disabled(true)
+ var errors = await compositor.update()
+ set_buttons_disabled(false)
+ if len(errors) > 0:
+ update_status(Status.ERROR, "\n".join(errors))
+ else:
+ update_status(Status.OKAY)
+ status_indicator.disabled = true
+
+func _on_save_image_button_pressed():
+ if Filesystem.result != null:
+ set_buttons_disabled(true)
+ save_image_dialog.current_path = Filesystem.last_image_savepath
+ save_image_dialog.show()
+
+#
+
+func _on_open_shader_dialog_file_selected(path: String):
+ Filesystem.shader_path = path
+ main.update_title(path.split("/")[-1])
+ self._on_apply_shader_button_pressed()
+
+func _on_open_shader_dialog_canceled() -> void:
+ set_buttons_disabled(false)
+
+func _on_open_shader_dialog_confirmed() -> void:
+ set_buttons_disabled(false)
+
+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)
+
+func _on_save_image_dialog_confirmed() -> void:
+ set_buttons_disabled(false)
+
+#
+
+func update_status(status: Status, msg: String = ""):
+ error_msg_dialog.dialog_text = msg
+ error_msg_dialog.reset_size()
+ if status == Status.OKAY:
+ status_indicator.texture_normal = status_okay_texture
+ elif status == Status.ERROR:
+ status_indicator.texture_normal = status_error_texture
+ else:
+ status_indicator.texture_normal = null
+ if msg == "":
+ status_indicator.disabled = true
+ else:
+ status_indicator.disabled = false
+
+func _on_status_indicator_pressed() -> void:
+ error_msg_dialog.show()
diff --git a/src/MainUI.gd.uid b/src/MainUI.gd.uid
new file mode 100644
index 0000000..0077ccd
--- /dev/null
+++ b/src/MainUI.gd.uid
@@ -0,0 +1 @@
+uid://bxgmf2ny7yuc8
diff --git a/src/ShaderDirectiveParser.gd.uid b/src/ShaderDirectiveParser.gd.uid
new file mode 100644
index 0000000..59f7f0e
--- /dev/null
+++ b/src/ShaderDirectiveParser.gd.uid
@@ -0,0 +1 @@
+uid://dw8bep14j4j3w
diff --git a/src/UIAppVersion.gd b/src/UIAppVersion.gd
deleted file mode 100644
index 60fe35a..0000000
--- a/src/UIAppVersion.gd
+++ /dev/null
@@ -1,4 +0,0 @@
-extends Label
-
-func _ready():
- text = ProjectSettings.get_setting("application/config/version")
diff --git a/src/UIWindow.gd b/src/UIWindow.gd
deleted file mode 100644
index 72836ce..0000000
--- a/src/UIWindow.gd
+++ /dev/null
@@ -1,4 +0,0 @@
-extends Window
-
-func _on_close_requested() -> void:
- get_tree().quit()
diff --git a/src/VersionLabel.gd b/src/VersionLabel.gd
new file mode 100644
index 0000000..c3c07e7
--- /dev/null
+++ b/src/VersionLabel.gd
@@ -0,0 +1,8 @@
+extends Label
+
+func _ready():
+ text = ProjectSettings.get_setting("application/config/name") \
+ + " " \
+ + ProjectSettings.get_setting("application/config/version") \
+ + " | Godot " \
+ + Engine.get_version_info()["string"]
diff --git a/src/VersionLabel.gd.uid b/src/VersionLabel.gd.uid
new file mode 100644
index 0000000..e731c41
--- /dev/null
+++ b/src/VersionLabel.gd.uid
@@ -0,0 +1 @@
+uid://bh0gpu3i2p47f
diff --git a/src/assets/bg.png b/src/assets/bg.png
new file mode 100644
index 0000000..fb67c19
Binary files /dev/null and b/src/assets/bg.png differ
diff --git a/src/assets/bg.png.import b/src/assets/bg.png.import
new file mode 100644
index 0000000..e61ed86
--- /dev/null
+++ b/src/assets/bg.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://d2nwchyd6huob"
+path="res://.godot/imported/bg.png-7c8713dd1fab321784216191fa747e53.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://src/assets/bg.png"
+dest_files=["res://.godot/imported/bg.png-7c8713dd1fab321784216191fa747e53.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/assets/error.svg b/src/assets/error.svg
similarity index 100%
rename from assets/error.svg
rename to src/assets/error.svg
diff --git a/assets/error.svg.import b/src/assets/error.svg.import
similarity index 76%
rename from assets/error.svg.import
rename to src/assets/error.svg.import
index 07666be..368b3a5 100644
--- a/assets/error.svg.import
+++ b/src/assets/error.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://04iv1gogpuhu"
-path="res://.godot/imported/error.svg-75fe5f417585e01e99de8885f1f45c3b.ctex"
+path="res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://assets/error.svg"
-dest_files=["res://.godot/imported/error.svg-75fe5f417585e01e99de8885f1f45c3b.ctex"]
+source_file="res://src/assets/error.svg"
+dest_files=["res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex"]
[params]
diff --git a/assets/icon.png b/src/assets/icon.png
similarity index 100%
rename from assets/icon.png
rename to src/assets/icon.png
diff --git a/assets/icon.png.import b/src/assets/icon.png.import
similarity index 73%
rename from assets/icon.png.import
rename to src/assets/icon.png.import
index ce38ae5..d1a6196 100644
--- a/assets/icon.png.import
+++ b/src/assets/icon.png.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://kqwc4avs2xdp"
-path="res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex"
+path="res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://assets/icon.png"
-dest_files=["res://.godot/imported/icon.png-b6a7fb2db36edd3d95dc42f1dc8c1c5d.ctex"]
+source_file="res://src/assets/icon.png"
+dest_files=["res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex"]
[params]
diff --git a/assets/okay.svg b/src/assets/okay.svg
similarity index 100%
rename from assets/okay.svg
rename to src/assets/okay.svg
diff --git a/assets/okay.svg.import b/src/assets/okay.svg.import
similarity index 76%
rename from assets/okay.svg.import
rename to src/assets/okay.svg.import
index 6b6c11f..e1631da 100644
--- a/assets/okay.svg.import
+++ b/src/assets/okay.svg.import
@@ -3,15 +3,15 @@
importer="texture"
type="CompressedTexture2D"
uid="uid://m1omb6g45vst"
-path="res://.godot/imported/okay.svg-7f6df15523471a86f27b6817e9528a4e.ctex"
+path="res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex"
metadata={
"vram_texture": false
}
[deps]
-source_file="res://assets/okay.svg"
-dest_files=["res://.godot/imported/okay.svg-7f6df15523471a86f27b6817e9528a4e.ctex"]
+source_file="res://src/assets/okay.svg"
+dest_files=["res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex"]
[params]
diff --git a/src/scenes/main.tscn b/src/scenes/main.tscn
new file mode 100644
index 0000000..14b19bd
--- /dev/null
+++ b/src/scenes/main.tscn
@@ -0,0 +1,194 @@
+[gd_scene load_steps=13 format=3 uid="uid://bjah7k4bxo044"]
+
+[ext_resource type="Script" uid="uid://5sbslwysin5a" path="res://src/Main.gd" id="1_64y3g"]
+[ext_resource type="Script" uid="uid://d106170kuigl3" path="res://src/ImageCompositor.gd" id="2_4ykh7"]
+[ext_resource type="Shader" uid="uid://ctk7jomfyx0fh" path="res://src/shader/ivd_outline.gdshader" id="3_0fllm"]
+[ext_resource type="Script" uid="uid://ctc4lhbdsoq7u" path="res://src/ImageViewportDisplay.gd" id="4_pbpx2"]
+[ext_resource type="Script" uid="uid://b6r8rigubdctk" path="res://src/Camera.gd" id="5_hkdq6"]
+[ext_resource type="Texture2D" uid="uid://d2nwchyd6huob" path="res://src/assets/bg.png" id="6_kokaf"]
+[ext_resource type="Theme" uid="uid://cwqlns34rj3vx" path="res://src/theme.tres" id="6_rjp5f"]
+[ext_resource type="Script" uid="uid://bxgmf2ny7yuc8" path="res://src/MainUI.gd" id="7_5puhk"]
+[ext_resource type="Script" uid="uid://bh0gpu3i2p47f" path="res://src/VersionLabel.gd" id="8_kod8x"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_y2ea0"]
+shader = ExtResource("3_0fllm")
+shader_parameter/zoom_level = Vector2(1, 1)
+
+[sub_resource type="ViewportTexture" id="ViewportTexture_lct1c"]
+viewport_path = NodePath("Compositor")
+
+[sub_resource type="LabelSettings" id="LabelSettings_6o860"]
+font_size = 12
+shadow_color = Color(0, 0, 0, 1)
+
+[node name="Main" type="Node2D"]
+script = ExtResource("1_64y3g")
+
+[node name="Compositor" type="SubViewport" parent="."]
+unique_name_in_owner = true
+script = ExtResource("2_4ykh7")
+
+[node name="ImageViewportDisplay" type="Sprite2D" parent="."]
+unique_name_in_owner = true
+material = SubResource("ShaderMaterial_y2ea0")
+texture = SubResource("ViewportTexture_lct1c")
+script = ExtResource("4_pbpx2")
+
+[node name="Camera" type="Camera2D" parent="."]
+unique_name_in_owner = true
+offset = Vector2(0, -64)
+script = ExtResource("5_hkdq6")
+
+[node name="CanvasLayerBg" type="CanvasLayer" parent="."]
+layer = -1
+
+[node name="Control" type="Control" parent="CanvasLayerBg"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="TextureRect" type="TextureRect" parent="CanvasLayerBg/Control"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+texture = ExtResource("6_kokaf")
+stretch_mode = 1
+
+[node name="CanvasLayer" type="CanvasLayer" parent="."]
+
+[node name="MainUI" type="Control" parent="CanvasLayer"]
+unique_name_in_owner = true
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+pivot_offset = Vector2(320, 320)
+theme = ExtResource("6_rjp5f")
+script = ExtResource("7_5puhk")
+
+[node name="OpenShaderDialog" type="FileDialog" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+title = "Load Shader"
+size = Vector2i(521, 175)
+ok_button_text = "Open"
+mode_overrides_title = false
+file_mode = 0
+access = 2
+filters = PackedStringArray("*.gdshader")
+use_native_dialog = true
+
+[node name="SaveImageDialog" type="FileDialog" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+title = "Export Image"
+size = Vector2i(661, 175)
+mode_overrides_title = false
+access = 2
+filters = PackedStringArray("*.png")
+use_native_dialog = true
+
+[node name="ErrorMessageDialog" type="AcceptDialog" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+auto_translate_mode = 1
+title = "Status"
+initial_position = 2
+size = Vector2i(256, 128)
+popup_window = true
+ok_button_text = "Close"
+
+[node name="OpenShaderButton" type="Button" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+layout_mode = 1
+offset_left = 16.0
+offset_top = 16.0
+offset_right = 128.0
+offset_bottom = 48.0
+text = "Open Shader"
+
+[node name="SaveImageButton" type="Button" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+layout_mode = 1
+offset_left = 144.0
+offset_top = 16.0
+offset_right = 216.0
+offset_bottom = 48.0
+disabled = true
+text = "Export"
+
+[node name="FitImageButton" type="Button" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -176.0
+offset_top = 16.0
+offset_right = -128.0
+offset_bottom = 48.0
+grow_horizontal = 0
+text = "Fit"
+
+[node name="ApplyShaderButton" type="Button" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -112.0
+offset_top = 16.0
+offset_right = -16.0
+offset_bottom = 48.0
+grow_horizontal = 0
+text = "Apply (F5)"
+
+[node name="StatusIndicator" type="TextureButton" parent="CanvasLayer/MainUI"]
+unique_name_in_owner = true
+layout_mode = 1
+anchors_preset = 1
+anchor_left = 1.0
+anchor_right = 1.0
+offset_left = -220.0
+offset_top = 21.0
+offset_right = -196.0
+offset_bottom = 45.0
+grow_horizontal = 0
+disabled = true
+ignore_texture_size = true
+stretch_mode = 0
+
+[node name="VersionLabel" type="Label" parent="CanvasLayer/MainUI"]
+layout_mode = 1
+anchors_preset = 12
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = 16.0
+offset_top = -24.0
+offset_right = -16.0
+grow_horizontal = 2
+grow_vertical = 0
+label_settings = SubResource("LabelSettings_6o860")
+horizontal_alignment = 1
+vertical_alignment = 1
+script = ExtResource("8_kod8x")
+
+[connection signal="canceled" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_canceled"]
+[connection signal="confirmed" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_confirmed"]
+[connection signal="file_selected" from="CanvasLayer/MainUI/OpenShaderDialog" to="CanvasLayer/MainUI" method="_on_open_shader_dialog_file_selected"]
+[connection signal="canceled" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_canceled"]
+[connection signal="confirmed" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_confirmed"]
+[connection signal="file_selected" from="CanvasLayer/MainUI/SaveImageDialog" to="CanvasLayer/MainUI" method="_on_save_image_dialog_file_selected"]
+[connection signal="pressed" from="CanvasLayer/MainUI/OpenShaderButton" to="CanvasLayer/MainUI" method="_on_open_shader_button_pressed"]
+[connection signal="pressed" from="CanvasLayer/MainUI/SaveImageButton" to="CanvasLayer/MainUI" method="_on_save_image_button_pressed"]
+[connection signal="pressed" from="CanvasLayer/MainUI/FitImageButton" to="CanvasLayer/MainUI" method="_on_fit_image_button_pressed"]
+[connection signal="pressed" from="CanvasLayer/MainUI/ApplyShaderButton" to="CanvasLayer/MainUI" method="_on_apply_shader_button_pressed"]
+[connection signal="pressed" from="CanvasLayer/MainUI/StatusIndicator" to="CanvasLayer/MainUI" method="_on_status_indicator_pressed"]
diff --git a/src/shader/ivd_outline.gdshader b/src/shader/ivd_outline.gdshader
index 14ce6e0..78bfe77 100644
--- a/src/shader/ivd_outline.gdshader
+++ b/src/shader/ivd_outline.gdshader
@@ -11,6 +11,6 @@ void fragment() {
UV.x > 1.0-t.x ||
UV.y > 1.0-t.y
) {
- COLOR = mix(COLOR, vec4(1.0), 0.5);
+ COLOR = mix(COLOR, vec4(0.5), 0.5);
}
}
diff --git a/src/shader/ivd_outline.gdshader.uid b/src/shader/ivd_outline.gdshader.uid
new file mode 100644
index 0000000..b42ac31
--- /dev/null
+++ b/src/shader/ivd_outline.gdshader.uid
@@ -0,0 +1 @@
+uid://ctk7jomfyx0fh
diff --git a/src/shader/template.gdshader b/src/shader/template.gdshader
deleted file mode 100644
index be6c5cc..0000000
--- a/src/shader/template.gdshader
+++ /dev/null
@@ -1,7 +0,0 @@
-shader_type canvas_item;
-
-//!load /path/to/your/image
-
-void fragment() {
- // Called for every pixel the material is visible on.
-}
diff --git a/src/theme.tres b/src/theme.tres
new file mode 100644
index 0000000..782d3e9
--- /dev/null
+++ b/src/theme.tres
@@ -0,0 +1,57 @@
+[gd_resource type="Theme" load_steps=4 format=3 uid="uid://cwqlns34rj3vx"]
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_bm5o2"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.1, 0.1, 0.1, 0.3)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(1, 1, 1, 0.27451)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_l0k8a"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.225, 0.225, 0.225, 0.6)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(1, 1, 1, 0.784314)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+
+[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_1dkyv"]
+content_margin_left = 4.0
+content_margin_top = 4.0
+content_margin_right = 4.0
+content_margin_bottom = 4.0
+bg_color = Color(0.1, 0.1, 0.1, 0.6)
+border_width_left = 1
+border_width_top = 1
+border_width_right = 1
+border_width_bottom = 1
+border_color = Color(1, 1, 1, 0.509804)
+corner_radius_top_left = 3
+corner_radius_top_right = 3
+corner_radius_bottom_right = 3
+corner_radius_bottom_left = 3
+corner_detail = 5
+
+[resource]
+Button/styles/disabled = SubResource("StyleBoxFlat_bm5o2")
+Button/styles/hover = SubResource("StyleBoxFlat_l0k8a")
+Button/styles/normal = SubResource("StyleBoxFlat_1dkyv")
diff --git a/tools/get_version.gd b/tools/get_version.gd
new file mode 100644
index 0000000..8384f1b
--- /dev/null
+++ b/tools/get_version.gd
@@ -0,0 +1,7 @@
+extends SceneTree
+
+# godot --headless --no-header -s tools/get_version.gd
+
+func _init() -> void:
+ print(ProjectSettings.get_setting("application/config/version"))
+ quit(0)
diff --git a/tools/get_version.gd.uid b/tools/get_version.gd.uid
new file mode 100644
index 0000000..e14829d
--- /dev/null
+++ b/tools/get_version.gd.uid
@@ -0,0 +1 @@
+uid://cdhqbascy6pvy