diff --git a/LICENSE b/LICENSE index 920dc4f..ba50b01 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,28 @@ -MIT License +BSD 3-Clause License -Copyright (c) 2024 Julian Müller (ChaoticByte) +Copyright (c) 2025, Julian Müller (ChaoticByte) -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: +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -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. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 4c3496d..c658569 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,33 @@ void fragment() { COLOR = hsv_offset(COLOR, 0.32, 0.2, 0.0); } ``` + +## Commandline interface + +You can run Fragmented from the commandline or scripts. + +> Note: Headless mode is not supported. Using the commandline interface still opens a window. + +### Usage + +``` +./Fragmented cmd --shader PATH [--load-image PATH] + + --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) + +``` + +You can also run `./Fragmented cmd help` to show the help message. + +#### Examples + +``` +./Fragmented cmd --shader ./examples/oklab.gdshader --output ./output.png +``` + +``` +./Fragmented cmd --shader ./examples/oklab.gdshader --load-image ~/Pictures/test.png --output ./output.png +``` diff --git a/examples/blur.gdshader b/examples/blur.gdshader new file mode 100644 index 0000000..8d08069 --- /dev/null +++ b/examples/blur.gdshader @@ -0,0 +1,8 @@ +shader_type canvas_item; + +//!load ./images/swamp.jpg +#include "res://shaderlib/blur.gdshaderinc" + +void fragment() { + COLOR = gaussian_blur(TEXTURE, UV, 48, 24.0); +} diff --git a/examples/color_and_pixelate.gdshader b/examples/color_and_pixelate.gdshader index 7c2c0ef..bf5f18a 100644 --- a/examples/color_and_pixelate.gdshader +++ b/examples/color_and_pixelate.gdshader @@ -1,6 +1,6 @@ shader_type canvas_item; -#include "res://shaderlib/hsv.gdshaderinc" +#include "res://shaderlib/colorspaces.gdshaderinc" #include "res://shaderlib/effects.gdshaderinc" //!load ./images/swamp.jpg @@ -8,6 +8,7 @@ shader_type canvas_item; void fragment() { COLOR = pixelate(TEXTURE, UV, 200.0); vec4 hsv = rgb2hsv(COLOR); - COLOR = hsv_offset(COLOR, 0.65, .42-(hsv.y*.3), -.125); - COLOR = hsv_multiply(COLOR, 1.0, 1.0, 1.25); + 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/oklab.gdshader b/examples/oklab.gdshader new file mode 100644 index 0000000..0d8f093 --- /dev/null +++ b/examples/oklab.gdshader @@ -0,0 +1,12 @@ +shader_type canvas_item; + +#include "res://shaderlib/colorspaces.gdshaderinc" + +//!load ./images/swamp.jpg + +void fragment() { + vec4 oklab = rgb2oklab(COLOR); + vec4 oklch = oklab2oklch(oklab); + oklch.z -= 2.0; + COLOR = oklab2rgb(oklch2oklab(oklch)); +} diff --git a/project.godot b/project.godot index c244ec2..a8dffd7 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Fragmented" -config/version="v6.2" +config/version="v7.0" run/main_scene="res://scenes/main.tscn" config/features=PackedStringArray("4.3", "Mobile") run/low_processor_mode=true diff --git a/scenes/main.tscn b/scenes/main.tscn index b43b0c8..2d104a3 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -1,7 +1,7 @@ [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/Compositor.gd" id="2_hvo65"] +[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"] @@ -20,14 +20,7 @@ script = ExtResource("1_2625y") [node name="Compositor" type="SubViewport" parent="."] unique_name_in_owner = true -disable_3d = true -transparent_bg = true -canvas_item_default_texture_filter = 0 -render_target_update_mode = 4 -script = ExtResource("2_hvo65") - -[node name="ImageSprite" type="Sprite2D" parent="Compositor"] -unique_name_in_owner = true +script = ExtResource("2_4thch") [node name="ImageViewportDisplay" type="Sprite2D" parent="."] unique_name_in_owner = true @@ -44,6 +37,7 @@ 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")] diff --git a/screenshot.png b/screenshot.png index 62b1635..19fb490 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/shaderlib/blur.gdshaderinc b/shaderlib/blur.gdshaderinc new file mode 100644 index 0000000..339256d --- /dev/null +++ b/shaderlib/blur.gdshaderinc @@ -0,0 +1,32 @@ + +/* + gaussian_blur adapted from https://godotshaders.com/shader/customizable-gausian-blur/ + original code by https://godotshaders.com/author/djbob-gaming-yt/ + maximum radius is 64 +*/ +vec4 gaussian_blur(sampler2D texture, vec2 uv, int radius, float sigma) { + vec2 resolution = 1.0 / vec2(textureSize(texture, 0)); + // calculate kernel + float kernel[64]; + float sum = 0.0; + for (int i = 0; i <= radius; i++) { + kernel[i] = exp(-0.5 * float(i * i) / (sigma * sigma)); + sum += i == 0 ? kernel[i] : 2.0 * kernel[i]; + } + for (int i = 0; i <= radius; i++) { + kernel[i] /= sum; + } + // + vec4 final_color = vec4(0.0); + float total_weight = 0.0; + for (int x = -radius; x <= radius; x++) { + for (int y = -radius; y <= radius; y++) { + float weight = kernel[abs(x)] * kernel[abs(y)]; + vec2 offset = vec2(float(x), float(y)) * resolution; + final_color += texture(texture, uv + offset) * weight; + total_weight += weight; + } + } + final_color /= total_weight; + return final_color; +} diff --git a/shaderlib/colorspaces.gdshaderinc b/shaderlib/colorspaces.gdshaderinc new file mode 100644 index 0000000..6a8ae9b --- /dev/null +++ b/shaderlib/colorspaces.gdshaderinc @@ -0,0 +1,90 @@ + +/* + 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 +*/ + +vec4 rgb2oklab(vec4 c) { + 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) { + 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) { + return vec4( + c.x, + sqrt((c.y * c.y) + (c.z * c.z)), + atan(c.z, c.y), + c.a + ); +} + +vec4 oklch2oklab(vec4 c) { + return vec4( + c.x, + c.y * cos(c.z), + c.y * sin(c.z), + c.a + ); +} 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/denoise.gdshaderinc b/shaderlib/denoise.gdshaderinc index 322f6ec..585104d 100644 --- a/shaderlib/denoise.gdshaderinc +++ b/shaderlib/denoise.gdshaderinc @@ -1,32 +1,34 @@ -/* glslSmartDenoise by Michele Morrone, adapted -original code: https://github.com/BrutPitt/glslSmartDeNoise -license of the original code: -BSD 2-Clause License +/* + glslSmartDenoise by Michele Morrone, adapted + original code: https://github.com/BrutPitt/glslSmartDeNoise + license of the original code: -Copyright (c) 2019-2020 Michele Morrone -All rights reserved. + BSD 2-Clause License -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: + Copyright (c) 2019-2020 Michele Morrone + All rights reserved. -1. Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: -2. Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #define INV_SQRT_OF_2PI 0.39894228040143267793994605993439 // 1.0/SQRT_OF_2PI diff --git a/shaderlib/hsv.gdshaderinc b/shaderlib/hsv.gdshaderinc deleted file mode 100644 index e1d9fdc..0000000 --- a/shaderlib/hsv.gdshaderinc +++ /dev/null @@ -1,50 +0,0 @@ - -// 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); -} - -// Offset the hue, saturation and brightness of a RGB color -vec4 hsv_offset( - vec4 rgba, - float offset_hue, - float offset_saturation, - float offset_brightness -) { - vec4 c = rgb2hsv(rgba); - c.x += offset_hue; - c.y += offset_saturation; - c.z += offset_brightness; - return hsv2rgb(c); -} - -// Multiply the hue, saturation and brightness of a RGB color -vec4 hsv_multiply( - vec4 rgba, - float mult_hue, - float mult_saturation, - float mult_brightness -) { - vec4 c = rgb2hsv(rgba); - c.x *= mult_hue; - c.y *= mult_saturation; - c.z *= mult_brightness; - return hsv2rgb(c); -} diff --git a/shaderlib/transform.gdshaderinc b/shaderlib/transform.gdshaderinc index c888732..af57cce 100644 --- a/shaderlib/transform.gdshaderinc +++ b/shaderlib/transform.gdshaderinc @@ -1,6 +1,8 @@ -// Load in a texture from a sampler2D with an offset and scale -// See examples/place_texture.gdshader +/* + Load in a texture from a sampler2D with an offset and scale + See examples/place_texture.gdshader +*/ vec4 place_texture(sampler2D sampler, vec2 uv, vec2 texture_pixel_size, vec2 offset, vec2 scale) { vec2 texture_size = vec2(textureSize(sampler, 0)); // position of current pixel; sample color c diff --git a/shaderlib/transparency.gdshaderinc b/shaderlib/transparency.gdshaderinc index 220f0ba..a97e4b7 100644 --- a/shaderlib/transparency.gdshaderinc +++ b/shaderlib/transparency.gdshaderinc @@ -1,6 +1,8 @@ -// Alpha Blending a over b after Bruce A. Wallace -// source: https://en.wikipedia.org/wiki/Alpha_compositing +/* + 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); diff --git a/src/Editor.gd b/src/Editor.gd index edc4633..4fb716c 100644 --- a/src/Editor.gd +++ b/src/Editor.gd @@ -129,18 +129,20 @@ const gdshader_preprocessor = [ ] # shaderlib var shaderlib_regex = { - "hsv": RegEx.create_from_string(r'\s*\#include\s+\"res\:\/\/shaderlib\/hsv\.gdshaderinc\"'), + "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\"') + "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 = { - "hsv": ["rgb2hsv", "hsv2rgb", "hsv_offset", "hsv_multiply"], + "colorspaces": ["rgb2hsv", "hsv2rgb", "oklab2rgb", "rgb2oklab", "oklab2oklch", "oklch2oklab"], "transform": ["place_texture"], "transparency": ["alpha_blend"], "effects": ["pixelate"], - "denoise": ["smart_denoise"] + "denoise": ["smart_denoise"], + "blur": ["gaussian_blur"], } # # configure Highlighter diff --git a/src/Compositor.gd b/src/ImageCompositor.gd similarity index 79% rename from src/Compositor.gd rename to src/ImageCompositor.gd index 0a8bf14..5999b88 100644 --- a/src/Compositor.gd +++ b/src/ImageCompositor.gd @@ -1,9 +1,22 @@ -extends SubViewport +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_sprite = %ImageSprite @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: @@ -31,7 +44,7 @@ func inject_step_uniform(shader_code: String) -> Shader: shader.code = shader_code.insert(fragment_function_match.get_start(), "\nuniform int STEP;") return shader -func update() -> Array: # returns error messages (strings) +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 steps: int = ShaderDirectiveParser.parse_steps_directive(shader.code) @@ -40,11 +53,15 @@ func update() -> Array: # returns error messages (strings) return ["Shader compilation failed!"] var errors = [] # load texture(s) from //!load directive -> TEXTURE - var m = ShaderDirectiveParser.parse_load_directive(shader.code) - if len(m) < 1: - errors.append("Didn't find a load directive!") - return errors - var original_image_path = Filesystem.get_absolute_path(m[1]) + var original_image_path = "" + if overwrite_image_path == "": + var m = ShaderDirectiveParser.parse_load_directive(shader.code) + if len(m) < 1: + errors.append("Didn't find a load directive!") + return errors + original_image_path = Filesystem.get_absolute_path(m[1]) + else: + original_image_path = overwrite_image_path var fit_image = false if original_image_path != Filesystem.last_original_image_path: fit_image = true diff --git a/src/Main.gd b/src/Main.gd index 2acdbf4..8e54a86 100644 --- a/src/Main.gd +++ b/src/Main.gd @@ -4,18 +4,66 @@ extends Node @onready var ui_container = %UserInterfaceContainer @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") + +func parse_custom_cmdline(args: PackedStringArray): + var kwargs: Dictionary = {"--shader": null, "--output": null, "--load-image": 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 _ready(): - 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) - # 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 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: + show_help() + get_tree().quit(1) + else: + Filesystem.load_shader(kwargs["--shader"]) + var errors = [] + if kwargs["--load-image"] == null: + errors = await $Compositor.update() + 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) + 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) func update_title(current_file: String = ""): if current_file == "":