diff --git a/README.md b/README.md new file mode 100644 index 0000000..275beb9 --- /dev/null +++ b/README.md @@ -0,0 +1,120 @@ + +


Pigment

+ +

Use Godot's GLSL dialect for color grading and more.

+ +![screenshot](./screenshot.png) + + +## Table of Contents + +- [Supported Platforms](#supported-platforms) +- [Usage](#usage) +- [Shaderlib](#shaderlib) +- [Commandline interface](#commandline-interface) +- [Known Issues](#known-issues) + + +## Supported Platforms + +- Linux + +You can find the latest release ... + +- [here (remotebranch.eu)](https://remotebranch.eu/ChaoticByte/Pigment/releases/latest) + + +## Supported Image Formats + +| Format | Import | Export | Notes | +| ------ | :----: | :----: | ----- | +| PNG | ✔ | ✔ | exports to 8-bit RGBA | +| JPEG | ✔ | | | +| WEBP | ✔ | | | + + +## Usage + +With Pigment, you process images by writing Shader code using Godot's GLSL dialect: GDShader. +If you want to learn GDShader, take a look at the [Godot docs](https://docs.godotengine.org/en/stable/tutorials/shaders/). + +To get started, use the project template (see the Releases section of this repo) and open it in Godot. The template includes examples that you can use as a starting-point to write your own stuff. + +After opening the project in Godot, start Pigment and open a `.gdshader` file & an image. Edit the shader using Godots `Shader Editor`, and hit `Apply` in Pigment. + + +## Shaderlib + +This repo comes with a (still small) shader library including pre-written functions and more. +Have a look at the `shaderlib` folder. + +Here is an example: + +```glsl +shader_type canvas_item; + +#include "./shaderlib/oklab.gdshaderinc" + +void fragment() { + vec4 oklab = rgb2oklab(COLOR); + vec4 oklch = oklab2oklch(oklab); + oklch.z -= 2.0; + COLOR = oklab2rgb(oklch2oklab(oklch)); +} +``` + + +## Commandline interface + +You can run Pigment from the commandline or scripts. + +> Note: Headless mode is not supported. Using the commandline interface still opens a window. + + +### Usage + +``` +~ Pigment CLI ~ +-=============- + +Usage: + +./Pigment + +Commands: + + help + + | Shows this help text. + + apply --shader PATH --input PATH --output PATH + + | Applies a shader file. + + --shader PATH The path to the shader + --input PATH The path to the image + Passing a folder activates batch mode + --output PATH Where to write the resulting image to + In batch mode, this must be a folder + +``` + + +### Batch Mode + +You can pass a directory to `--input` and `--output`. This will process all images in the input directory and write the output to the output directory. + +> Note: This is not very fast, so using it on maaany images may take some time. + + +#### Examples + +``` +./Pigment apply --shader ./examples/oklab.gdshader --input ~/Pictures/test.png --output ./output.png +``` + + +## Known Issues + +- On some systems, screen scaling could lead to an either blurry UI, or no scaling at all +- CLI: Godot's `--headless` option is not supported diff --git a/dist.sh b/dist.sh new file mode 100755 index 0000000..52caf1b --- /dev/null +++ b/dist.sh @@ -0,0 +1,32 @@ +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/Pigment-${VERSION}.x86_64" +godot --headless --export-release "Windows Desktop" "dist/Pigment-${VERSION}.exe" + +log Packing shaderlib + +ZIP_PATH_SHADERLIB=$(realpath "dist/Pigment-${VERSION}_shaderlib.zip") + +zip -r "${ZIP_PATH_SHADERLIB}" shaderlib/ + +log Packing project template + +ZIP_PATH_PROJECT_TEMPLATE=$(realpath "dist/Pigment-${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/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/greyscale.gdshader b/examples/greyscale.gdshader new file mode 100644 index 0000000..d7f4c1f --- /dev/null +++ b/examples/greyscale.gdshader @@ -0,0 +1,8 @@ +shader_type canvas_item; + +void fragment() { + float v = (COLOR.r + COLOR.g + COLOR.b) / 3.0; + COLOR.r = v; + COLOR.g = v; + COLOR.b = v; +} 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/hsv.gdshader b/examples/hsv.gdshader new file mode 100644 index 0000000..20b2547 --- /dev/null +++ b/examples/hsv.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; + +#include "./shaderlib/hsv.gdshaderinc" + +void fragment() { + vec4 hsv = rgb2hsv(COLOR); + hsv.xyz += vec3(0.65, .42-(hsv.y*.3), -.125); + hsv.xyz *= vec3(1.0, 1.0, 1.25); + COLOR = hsv2rgb(hsv); +} diff --git a/examples/hsv.gdshader.uid b/examples/hsv.gdshader.uid new file mode 100644 index 0000000..d943067 --- /dev/null +++ b/examples/hsv.gdshader.uid @@ -0,0 +1 @@ +uid://gd23hu7ro148 diff --git a/examples/images/CREDITS.md b/examples/images/CREDITS.md new file mode 100644 index 0000000..4b7dfd5 --- /dev/null +++ b/examples/images/CREDITS.md @@ -0,0 +1,5 @@ + +# Example Images + +- swamp.jpg by [clfr21 on Pixabay](https://pixabay.com/de/users/clfr21-6530007/) +- 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..9288ecc --- /dev/null +++ b/examples/images/mountain.jpg.import @@ -0,0 +1,40 @@ +[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/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/images/swamp.jpg b/examples/images/swamp.jpg new file mode 100644 index 0000000..d7bdf3c Binary files /dev/null and b/examples/images/swamp.jpg differ diff --git a/examples/images/swamp.jpg.import b/examples/images/swamp.jpg.import new file mode 100644 index 0000000..50879f2 --- /dev/null +++ b/examples/images/swamp.jpg.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckjb0agn5btv7" +path="res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://examples/images/swamp.jpg" +dest_files=["res://.godot/imported/swamp.jpg-1dfdcd52a5ef03d42a82a7f06acefa98.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/lowpass.gdshader b/examples/lowpass.gdshader new file mode 100644 index 0000000..04d1dec --- /dev/null +++ b/examples/lowpass.gdshader @@ -0,0 +1,9 @@ +shader_type canvas_item; + +const float threshold = 0.6; + +void fragment() { + vec4 tex = texture(TEXTURE , UV); + COLOR.rgb = min(tex.rgb, vec3(threshold)); + COLOR.a = tex.a; +} 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/oklab.gdshader b/examples/oklab.gdshader new file mode 100644 index 0000000..b01c285 --- /dev/null +++ b/examples/oklab.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; + +#include "./shaderlib/oklab.gdshaderinc" + +void fragment() { + vec4 oklab = rgb2oklab(COLOR); + vec4 oklch = oklab2oklch(oklab); + oklch.z -= 2.0; + COLOR = oklab2rgb(oklch2oklab(oklch)); +} 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/project.godot_ b/examples/project.godot_ new file mode 100644 index 0000000..9ff54b2 --- /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="Pigment Project" +run/main_scene="res://0_empty.tscn" +config/features=PackedStringArray("4.5", "Mobile") 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/export_presets.cfg b/export_presets.cfg new file mode 100644 index 0000000..b543db3 --- /dev/null +++ b/export_presets.cfg @@ -0,0 +1,114 @@ +[preset.0] + +name="Linux/X11" +platform="Linux" +runnable=true +advanced_options=true +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="screenshot.png, examples/*, shaderlib/*, tools/*" +export_path="dist/Pigment.x86_64" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=false +binary_format/architecture="x86_64" +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="#!/usr/bin/env bash +export DISPLAY=:0 +unzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\" +\"{temp_dir}/{exe_name}\" {cmd_args}" +ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash +kill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\") +rm -rf \"{temp_dir}\"" +texture_format/bptc=true +texture_format/s3tc=true +texture_format/etc=false +texture_format/etc2=false + +[preset.1] + +name="Windows Desktop" +platform="Windows Desktop" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="dist/Pigment.exe" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.1.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=1 +binary_format/embed_pck=true +texture_format/s3tc_bptc=true +texture_format/etc2_astc=false +shader_baker/enabled=false +binary_format/architecture="x86_64" +codesign/enable=false +codesign/timestamp=true +codesign/timestamp_server_url="" +codesign/digest_algorithm=1 +codesign/description="" +codesign/custom_options=PackedStringArray() +application/modify_resources=true +application/icon="" +application/console_wrapper_icon="" +application/icon_interpolation=4 +application/file_version="" +application/product_version="" +application/company_name="" +application/product_name="" +application/file_description="" +application/copyright="" +application/trademarks="" +application/export_angle=0 +application/export_d3d12=0 +application/d3d12_agility_sdk_multiarch=true +ssh_remote_deploy/enabled=false +ssh_remote_deploy/host="user@host_ip" +ssh_remote_deploy/port="22" +ssh_remote_deploy/extra_args_ssh="" +ssh_remote_deploy/extra_args_scp="" +ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}' +$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}' +$trigger = New-ScheduledTaskTrigger -Once -At 00:00 +$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries +$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings +Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true +Start-ScheduledTask -TaskName godot_remote_debug +while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 } +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue" +ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue +Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue +Remove-Item -Recurse -Force '{temp_dir}'" diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..e9c00e0 --- /dev/null +++ b/project.godot @@ -0,0 +1,72 @@ +; 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="Pigment" +config/version="v1.0" +run/main_scene="res://src/scenes/main.tscn" +config/features=PackedStringArray("4.5", "Mobile") +config/icon="uid://kqwc4avs2xdp" + +[autoload] + +Filesystem="*res://src/Filesystem.gd" + +[display] + +window/size/viewport_width=640 +window/size/viewport_height=672 +window/energy_saving/keep_screen_on=false +window/subwindows/embed_subwindows=false + +[editor_plugins] + +enabled=PackedStringArray() + +[input] + +zoom_out={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":16,"position":Vector2(244, 15),"global_position":Vector2(248, 56),"factor":1.0,"button_index":5,"canceled":false,"pressed":true,"double_click":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":45,"physical_keycode":0,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194435,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null) +] +} +zoom_in={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":8,"position":Vector2(270, 19),"global_position":Vector2(274, 60),"factor":1.0,"button_index":4,"canceled":false,"pressed":true,"double_click":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":43,"physical_keycode":0,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null) +, Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194437,"key_label":0,"unicode":43,"location":0,"echo":false,"script":null) +] +} +drag={ +"deadzone": 0.5, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(183, 23),"global_position":Vector2(187, 64),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null) +] +} +apply_shader={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":4194336,"physical_keycode":0,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +save_shader={ +"deadzone": 0.5, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":true,"meta_pressed":false,"pressed":false,"keycode":83,"physical_keycode":0,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} + +[rendering] + +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.501961, 0.501961, 0.501961, 1) diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000..d7db167 Binary files /dev/null and b/screenshot.png differ diff --git a/shaderlib/common.gdshaderinc b/shaderlib/common.gdshaderinc new file mode 100644 index 0000000..7ee57f8 --- /dev/null +++ b/shaderlib/common.gdshaderinc @@ -0,0 +1,5 @@ + +// inefficient cuberoot function +float cbrt(float x) { + return pow(x, 1.0/3.0); +} 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/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/oklab.gdshaderinc b/shaderlib/oklab.gdshaderinc new file mode 100644 index 0000000..5a456c7 --- /dev/null +++ b/shaderlib/oklab.gdshaderinc @@ -0,0 +1,70 @@ + +/* + 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; + + float l_ = cbrt(l); + float m_ = cbrt(m); + float s_ = cbrt(s); + + return vec4( + 0.2104542553f*l_ + 0.7936177850f*m_ - 0.0040720468f*s_, + 1.9779984951f*l_ - 2.4285922050f*m_ + 0.4505937099f*s_, + 0.0259040371f*l_ + 0.7827717662f*m_ - 0.8086757660f*s_, + c.a + ); +} + +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; + + float l = l_*l_*l_; + float m = m_*m_*m_; + float s = s_*s_*s_; + + return vec4( + +4.0767416621f * l - 3.3077115913f * m + 0.2309699292f * s, + -1.2684380046f * l + 2.6097574011f * m - 0.3413193965f * s, + -0.0041960863f * l - 0.7034186147f * m + 1.7076147010f * s, + c.a + ); +} + +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)), + atan(c.z, c.y), + c.a + ); +} + +vec4 oklch2oklab(vec4 c) { + // oklch.z (hue) ranges from -3.6 to 3.6 + return vec4( + c.x, + c.y * cos(c.z), + c.y * sin(c.z), + c.a + ); +} 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/src/Camera.gd b/src/Camera.gd new file mode 100644 index 0000000..8d61a49 --- /dev/null +++ b/src/Camera.gd @@ -0,0 +1,50 @@ +extends Camera2D + +@onready var image_viewport_display = %ImageViewportDisplay + +var drag = false + +func _input(event): + if event.is_action_pressed("zoom_out"): + zoom_out() + elif event.is_action_pressed("zoom_in"): + zoom_in() + if event.is_action_pressed("drag"): + self.drag = true + elif event.is_action_released("drag"): + self.drag = false + 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.input_image_texture != null: + var image_size = Filesystem.input_image_texture.get_size() + var viewport_size = get_viewport_rect().size + 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 zoom_in(): + var old_mouse_pos = get_global_mouse_position() + self.zoom *= 1.2 + self.global_position += old_mouse_pos - get_global_mouse_position() + +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() + +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/Filesystem.gd b/src/Filesystem.gd new file mode 100644 index 0000000..f6f1d9e --- /dev/null +++ b/src/Filesystem.gd @@ -0,0 +1,61 @@ +extends Node + +const SUPPORTED_INPUT_EXT = [ + ".jpeg", ".jpg", ".png", ".webp" +] + +var cwd = "." + +var shader_path = "": + get(): + return shader_path + set(v): + shader_path = v + if "/" in v: # update current working directory + cwd = v.substr(0, v.rfind("/")) + +var shader: Shader: + get(): + if shader_path == "": + return null + print("Load ", shader_path) + return load(shader_path) + +var input_image_path = "" +var input_image_texture: ImageTexture +var last_input_image_path = "" + +var last_image_savepath = "" +var result: Image + +func get_absolute_path(p: String) -> String: + # this only works on Linux! + if !p.begins_with("/"): + return self.cwd + "/" + p.lstrip("./") + return p + +func load_image() -> String: # returns an error message + var supported = false + for e in SUPPORTED_INPUT_EXT: + if self.input_image_path.ends_with(e): + supported = true + if not supported: + return "File extension not supported." + print("Load ", self.input_image_path) + var img = Image.new() + var err = img.load(self.input_image_path) + if err == OK: + input_image_texture = ImageTexture.create_from_image(img) + if self.last_image_savepath == "" or self.input_image_path != self.last_input_image_path: + self.last_image_savepath = self.input_image_path + self.last_input_image_path = self.input_image_path + return "" + return error_string(err) + " " + self.input_image_path + +func save_result(path: String): + print("Export ", path) + var err = self.result.save_png(path) + if err != OK: + print("An error occured!") + else: + self.last_image_savepath = path 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 new file mode 100644 index 0000000..4fa083c --- /dev/null +++ b/src/ImageCompositor.gd @@ -0,0 +1,89 @@ +class_name ImageCompositor extends SubViewport + +var image_sprite: Sprite2D + +func _init() -> void: + # Overwrite some variables + self.render_target_update_mode = SubViewport.UPDATE_ALWAYS + self.disable_3d = true + self.transparent_bg = true + self.canvas_item_default_texture_filter = Viewport.DEFAULT_CANVAS_ITEM_TEXTURE_FILTER_NEAREST + self.image_sprite = Sprite2D.new() + +@onready var camera = %Camera +@onready var image_viewport_display = %ImageViewportDisplay + +func _ready() -> void: + # Add image sprite as child to be rendered + self.add_child(image_sprite) + +var _fragment_function_regex: RegEx = RegEx.create_from_string(r'\s*void\s+fragment\s*\(\s*\)\s*{\s*') + +func validate_shader_compilation(shader: Shader) -> bool: + # Inject code to validate shader compilation + var shader_code = shader.code; + # -> get position of fragment shader + var fragment_function_match = _fragment_function_regex.search(shader.code) + if fragment_function_match == null: + return false + # -> inject uniform + var uniform_name = "shader_compilation_validate_" + str(randi_range(999999999, 100000000)) + var uniform_code_line = "\nuniform bool " + uniform_name + ";\n" + shader_code = shader_code.insert(fragment_function_match.get_start(), uniform_code_line) + # -> inject variable access to prevent that the uniform gets optimized away + shader_code = shader_code.insert(fragment_function_match.get_end() + len(uniform_code_line), "\n" + uniform_name + ";\n") + # apply shader code + shader.code = shader_code + # 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_uniform(shader: Shader, var_name: String, type: int) -> bool: + for u in shader.get_shader_uniform_list(): + if u["name"] == var_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() -> Array: # returns error messages (strings) + if Filesystem.input_image_path == "": + return ["No image loaded!"] + var shader = Filesystem.shader # read from disk + if shader == null: + return ["No shader opened!"] + # validate shader + if not validate_shader_compilation(shader): + return ["Shader compilation failed!"] + var errors = [] + # load texture + var fit_image = Filesystem.input_image_path != Filesystem.last_input_image_path + var err = Filesystem.load_image() + if err != "": + errors.append(err) + image_viewport_display.hide() + return errors + # apply texture + image_sprite.texture = Filesystem.input_image_texture + image_sprite.offset = Filesystem.input_image_texture.get_size() / 2 + self.size = Filesystem.input_image_texture.get_size() + # 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 + # assign material + image_sprite.material = mat + set_vsync(false) # speed up processing + # wait for next frame & 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 + # 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 b/src/ImageViewportDisplay.gd new file mode 100644 index 0000000..7a9c827 --- /dev/null +++ b/src/ImageViewportDisplay.gd @@ -0,0 +1,10 @@ +extends Sprite2D + +func _ready() -> void: + hide() + +func update_zoom_texture_filter(zoom: Vector2): + if zoom.x >= 1.5: + texture_filter = TEXTURE_FILTER_NEAREST_WITH_MIPMAPS + else: + texture_filter = TEXTURE_FILTER_LINEAR 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 new file mode 100644 index 0000000..ef3cbe7 --- /dev/null +++ b/src/Main.gd @@ -0,0 +1,114 @@ +extends Node + +@onready var app_name = ProjectSettings.get_setting("application/config/name") + +func show_help(): + print( + "Usage:\n\n", + "./Pigment \n\n", + "Commands:\n\n", + " help\n\n", + " | Shows this help text.\n\n", + " apply --shader PATH --input PATH --output PATH\n\n", + " | Applies a shader file.\n\n", + " --shader PATH The path to the shader\n", + " --input PATH The path to the image\n", + " Passing a folder activates batch mode\n", + " --output PATH Where to write the resulting image to\n", + " In batch mode, this must be a folder\n") + +func parse_custom_cmdline(args: PackedStringArray): + var kwargs: Dictionary = {"--shader": null, "--output": null, "--input": null} + var args_len = args.size() + var i = 0 + while i < args_len: + var a = args[i] + if a in kwargs && args_len > i+1: + i += 1 + kwargs[a] = args[i] + i += 1 + return kwargs + +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( + "~ Pigment 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 or kwargs["--input"] == null: + show_help() + get_tree().quit(1) + return + var batch_mode = false + var load_image_dir: DirAccess + load_image_dir = DirAccess.open(kwargs["--input"]) + if load_image_dir != null: + # batch mode + if DirAccess.open(kwargs["--output"]) == null: + printerr("If --input is a directory, --output has to be one too.\n") + show_help() + get_tree().quit(1) + return + else: + 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 Filesystem.SUPPORTED_INPUT_EXT: + 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: + get_tree().quit(1) + return + get_tree().quit(0) + else: + var errors = [] + Filesystem.input_image_path = kwargs["--input"] + errors = await $Compositor.update() + 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() + +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" + else: + get_window().title = current_file + " - " + app_name + " - Viewer" 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..4de65b1 --- /dev/null +++ b/src/MainUI.gd @@ -0,0 +1,135 @@ +extends Control + +@onready var open_shader_dialog = %OpenShaderDialog +@onready var open_image_dialog = %OpenImageDialog +@onready var save_image_dialog = %SaveImageDialog + +@onready var open_shader_button = %OpenShaderButton +@onready var open_image_button = %OpenImageButton +@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 _ready() -> void: + var exts = [] + for e in Filesystem.SUPPORTED_INPUT_EXT: + exts.append("*" + e) + open_image_dialog.add_filter(", ".join(exts), "Image") + open_shader_dialog.add_filter("*.gdshader", "Shader") + save_image_dialog.add_filter("*.png", "Image") + +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): + var btns = [] + if disabled or (Filesystem.input_image_path != "" and Filesystem.shader_path != ""): + btns = [open_shader_button, open_image_button, save_image_button, fit_image_button, apply_shader_button, status_indicator] + else: + btns = [open_shader_button, open_image_button, fit_image_button, apply_shader_button, status_indicator] + for b in btns: + b.disabled = disabled +# + +func _on_open_shader_button_pressed(): + set_buttons_disabled(true) + open_shader_dialog.show() + +func _on_open_image_button_pressed(): + set_buttons_disabled(true) + open_image_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) + var p = Filesystem.last_image_savepath + for ext in Filesystem.SUPPORTED_INPUT_EXT: + p = p.trim_suffix(ext) + save_image_dialog.current_path = p + ".png" + 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_open_image_dialog_file_selected(path: String) -> void: + Filesystem.input_image_path = path + self._on_apply_shader_button_pressed() + +func _on_open_image_dialog_canceled() -> void: + set_buttons_disabled(false) + +func _on_open_image_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/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..aeaccf6 --- /dev/null +++ b/src/assets/bg.png.import @@ -0,0 +1,40 @@ +[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/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/src/assets/error.svg b/src/assets/error.svg new file mode 100644 index 0000000..af2f066 --- /dev/null +++ b/src/assets/error.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/assets/error.svg.import b/src/assets/error.svg.import new file mode 100644 index 0000000..0fe0b4f --- /dev/null +++ b/src/assets/error.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://04iv1gogpuhu" +path="res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/assets/error.svg" +dest_files=["res://.godot/imported/error.svg-28fb29635cf59d39cabf7052619f602f.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/assets/icon.png b/src/assets/icon.png new file mode 100644 index 0000000..528bb00 Binary files /dev/null and b/src/assets/icon.png differ diff --git a/src/assets/icon.png.import b/src/assets/icon.png.import new file mode 100644 index 0000000..3a27dab --- /dev/null +++ b/src/assets/icon.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kqwc4avs2xdp" +path="res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/assets/icon.png" +dest_files=["res://.godot/imported/icon.png-d8298ab6eda392a806be6bb7eec65b9c.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/src/assets/icon.pxo b/src/assets/icon.pxo new file mode 100644 index 0000000..2a7a2b5 Binary files /dev/null and b/src/assets/icon.pxo differ diff --git a/src/assets/okay.svg b/src/assets/okay.svg new file mode 100644 index 0000000..5668fe8 --- /dev/null +++ b/src/assets/okay.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + diff --git a/src/assets/okay.svg.import b/src/assets/okay.svg.import new file mode 100644 index 0000000..8822478 --- /dev/null +++ b/src/assets/okay.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://m1omb6g45vst" +path="res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://src/assets/okay.svg" +dest_files=["res://.godot/imported/okay.svg-de66a022ef37753b085371b7c60aefd1.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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 +svg/scale=2.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/src/scenes/main.tscn b/src/scenes/main.tscn new file mode 100644 index 0000000..8289af5 --- /dev/null +++ b/src/scenes/main.tscn @@ -0,0 +1,240 @@ +[gd_scene load_steps=15 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"] +[ext_resource type="Shader" uid="uid://brjouqq6wqp5u" path="res://src/shader/uibarbg.gdshader" id="9_lp058"] + +[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="ShaderMaterial" id="ShaderMaterial_24j03"] +shader = ExtResource("9_lp058") + +[sub_resource type="LabelSettings" id="LabelSettings_6o860"] +font_size = 12 + +[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, -32) +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="TopBarBg" type="ColorRect" parent="CanvasLayer/MainUI"] +material = SubResource("ShaderMaterial_24j03") +layout_mode = 1 +anchors_preset = 10 +anchor_right = 1.0 +offset_bottom = 48.0 +grow_horizontal = 2 +color = Color(0, 0, 0, 0.39215687) + +[node name="BottomBarBg" type="ColorRect" parent="CanvasLayer/MainUI"] +material = SubResource("ShaderMaterial_24j03") +layout_mode = 1 +anchors_preset = 12 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -26.0 +grow_horizontal = 2 +grow_vertical = 0 +color = Color(0, 0, 0, 0.39215687) + +[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 +use_native_dialog = true + +[node name="OpenImageDialog" type="FileDialog" parent="CanvasLayer/MainUI"] +unique_name_in_owner = true +auto_translate_mode = 1 +title = "Load Image" +size = Vector2i(521, 175) +ok_button_text = "Open" +mode_overrides_title = false +file_mode = 0 +access = 2 +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 +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 = 8.0 +offset_right = 128.0 +offset_bottom = 40.0 +text = "Open Shader" + +[node name="OpenImageButton" type="Button" parent="CanvasLayer/MainUI"] +unique_name_in_owner = true +layout_mode = 1 +offset_left = 144.0 +offset_top = 8.249329 +offset_right = 256.0 +offset_bottom = 40.24933 +text = "Open Image" + +[node name="SaveImageButton" type="Button" parent="CanvasLayer/MainUI"] +unique_name_in_owner = true +layout_mode = 1 +offset_left = 272.0 +offset_top = 8.249329 +offset_right = 344.0 +offset_bottom = 40.24933 +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 = 8.249329 +offset_right = -128.0 +offset_bottom = 40.24933 +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 = 8.249329 +offset_right = -16.0 +offset_bottom = 40.24933 +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 = 13.0 +offset_right = -196.0 +offset_bottom = 37.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/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_dialog_canceled"] +[connection signal="confirmed" from="CanvasLayer/MainUI/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_dialog_confirmed"] +[connection signal="file_selected" from="CanvasLayer/MainUI/OpenImageDialog" to="CanvasLayer/MainUI" method="_on_open_image_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/OpenImageButton" to="CanvasLayer/MainUI" method="_on_open_image_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 new file mode 100644 index 0000000..7f3c267 --- /dev/null +++ b/src/shader/ivd_outline.gdshader @@ -0,0 +1,17 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform vec2 zoom_level = vec2(1.0); +const float thickness = 3.0; + +void fragment() { + vec2 t = thickness * TEXTURE_PIXEL_SIZE / zoom_level; + if ( + UV.x < t.x || + UV.y < t.y || + UV.x > 1.0-t.x || + UV.y > 1.0-t.y + ) { + 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/uibarbg.gdshader b/src/shader/uibarbg.gdshader new file mode 100644 index 0000000..86928c8 --- /dev/null +++ b/src/shader/uibarbg.gdshader @@ -0,0 +1,11 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D screen_texture: hint_screen_texture, filter_linear_mipmap; + +void fragment() { + vec4 screen = textureLod(screen_texture, SCREEN_UV, 4.0); + vec3 col = mix(screen.rgb, COLOR.rgb, COLOR.a); + COLOR.rgb = col; + COLOR.a = 1.0; +} diff --git a/src/shader/uibarbg.gdshader.uid b/src/shader/uibarbg.gdshader.uid new file mode 100644 index 0000000..3293e80 --- /dev/null +++ b/src/shader/uibarbg.gdshader.uid @@ -0,0 +1 @@ +uid://brjouqq6wqp5u 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