diff --git a/README.md b/README.md index bf5d780..f124cc8 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Just load an image using `//!load`, edit the shader code and hit `F5` to see the //!load ``` -The image file will be read and available as the `TEXTURE` variable. +The main image file will be read and available as the sampler2D `TEXTURE`. #### Load additional images @@ -34,6 +34,28 @@ uniform sampler2D ; Have a look at the `place_texture.gdshader` example. +### Have multiple steps with `//!steps n` + +You can apply your shaderfile multiple times. At every additional step, `TEXTURE` is the result of the previous step. This can be used to chain effects that cannot be easily chained otherwise. + +To query the current step index, a `STEP` uniform is automatically injected. If `steps` is set to `0`, your shader won't be applied at all. + +Example: + +```glsl +//!load ... +//!steps 5 + +void fragment() { + if (STEP == 0) { + ... + } else if (STEP == 1) { + ... + } + // ... and so on +} +``` + ## Shaderlib This repo comes with a (still small) shader library including pre-written functions and more. diff --git a/examples/rgb_uv_distort.gdshader b/examples/rgb_uv_distort.gdshader index 1b65816..d4cc0ba 100644 --- a/examples/rgb_uv_distort.gdshader +++ b/examples/rgb_uv_distort.gdshader @@ -1,13 +1,20 @@ shader_type canvas_item; +//!steps 9 //!load ./swamp.jpg -const float strength = 0.1; +const float strength = 0.01; void fragment() { + float v; + if (STEP % 3 == 0) { + v = COLOR.r; // 3 times + } else if (STEP % 3 == 0) { + v = COLOR.g; // 3 times + } else { + v = COLOR.b; // 3 times + } vec2 uv = UV; - float b = (COLOR.r + COLOR.g + COLOR.b) / 3.0; - uv.x = mix(uv.x, b, strength); - uv.y = mix(uv.y, b, strength); + uv.y -= v * strength; COLOR = texture(TEXTURE, uv); } diff --git a/scenes/main.tscn b/scenes/main.tscn index e0b45f8..b43b0c8 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -42,7 +42,7 @@ script = ExtResource("8_mls06") [node name="EditorWindow" type="Window" parent="."] unique_name_in_owner = true disable_3d = true -position = Vector2i(48, 80) +position = Vector2i(48, 36) size = Vector2i(704, 704) script = ExtResource("6_8k0ha") diff --git a/src/Compositor.gd b/src/Compositor.gd index ecd0c24..c7b584a 100644 --- a/src/Compositor.gd +++ b/src/Compositor.gd @@ -4,15 +4,10 @@ extends SubViewport @onready var image_sprite = %ImageSprite @onready var image_viewport_display = %ImageViewportDisplay -var _fragment_function_regex: RegEx +var _fragment_function_regex: RegEx = RegEx.create_from_string(r'\s*void\s+fragment\s*\(\s*\)\s*{\s*') -func _init(): - _fragment_function_regex = RegEx.new() - _fragment_function_regex.compile(r'\s*void\s+fragment\s*\(\s*\)\s*{\s*') - -func validate_shader_compilation() -> bool: +func validate_shader_compilation(shader: Shader) -> bool: # Inject code to validate shader compilation - var shader: Shader = Filesystem.shader.duplicate() var shader_code = shader.code; # -> get position of fragment shader var fragment_function_match = _fragment_function_regex.search(shader.code) @@ -29,19 +24,28 @@ func validate_shader_compilation() -> 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 update() -> Array: # returns error messages (strings) - if not validate_shader_compilation(): + # inject STEP uniform & get number of steps + var shader: Shader = inject_step_uniform(Filesystem.shader_code) + var step: int = ShaderDirectiveParser.parse_steps_directive(shader) + # validate shader + if not validate_shader_compilation(shader): return ["Shader compilation failed!"] var errors = [] - var fit_image = false - # load texture(s) - Filesystem.clear_additional_images() - # ... from //!load directive -> TEXTURE - var m = ShaderDirectiveParser.parse_load_directive(Filesystem.shader) + # load texture(s) from //!load directive -> TEXTURE + var m = ShaderDirectiveParser.parse_load_directive(shader) 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 fit_image = false if original_image_path != Filesystem.last_original_image_path: fit_image = true var err = Filesystem.load_original_image(original_image_path) @@ -50,7 +54,8 @@ func update() -> Array: # returns error messages (strings) image_viewport_display.hide() return errors # ... from //!load+ directives - for n in ShaderDirectiveParser.parse_load_additional_directive(Filesystem.shader): + Filesystem.clear_additional_images() + for n in ShaderDirectiveParser.parse_load_additional_directive(shader): err = Filesystem.load_additional_image(n[1], Filesystem.get_absolute_path(n[2])) if err != "": errors.append(err) @@ -60,22 +65,27 @@ func update() -> Array: # returns error messages (strings) image_sprite.texture = Filesystem.original_image image_sprite.offset = Filesystem.original_image.get_size() / 2 self.size = Filesystem.original_image.get_size() + # create shader material var mat = ShaderMaterial.new() - mat.shader = Filesystem.shader - # ... as shader parameters + mat.shader = shader + # add images as shader parameters for key in Filesystem.additional_images: mat.set_shader_parameter( key, # uniform param name Filesystem.additional_images[key]) - # assign material - image_sprite.material = mat - # Get viewport texture - await RenderingServer.frame_post_draw # for good measure - Filesystem.result = get_texture().get_image() - image_sprite.material = null - image_sprite.texture = ImageTexture.create_from_image(Filesystem.result) - if fit_image: - camera.fit_image() + # iterate n times + for i in range(step): + # set STEP param + mat.set_shader_parameter("STEP", i) + # assign material + image_sprite.material = mat + # Get viewport texture + await RenderingServer.frame_post_draw # for good measure + Filesystem.result = get_texture().get_image() + image_sprite.material = null + image_sprite.texture = ImageTexture.create_from_image(Filesystem.result) + if fit_image: + camera.fit_image() camera.update_vd_zoomlevel() image_viewport_display.show() # done diff --git a/src/Editor.gd b/src/Editor.gd index ea0c7fd..69f9aab 100644 --- a/src/Editor.gd +++ b/src/Editor.gd @@ -208,7 +208,7 @@ func _input(event): _on_save_shader_button_pressed() func update_code_edit(): - code_editor.text = Filesystem.shader.code + code_editor.text = Filesystem.shader_code enum Status {OKAY, ERROR, UNKNOWN = -1} @@ -239,14 +239,14 @@ func _on_open_shader_button_pressed(): open_shader_dialog.show() func _on_save_shader_button_pressed(): - Filesystem.shader.code = code_editor.text + 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 + Filesystem.shader_code = code_editor.text if Filesystem.last_shader_savepath == "": save_shader_dialog.current_file = "filter.gdshader" else: @@ -257,7 +257,7 @@ func _on_fit_image_button_pressed(): camera.fit_image() func _on_apply_shader_button_pressed(): - Filesystem.shader.code = code_editor.text + Filesystem.shader_code = code_editor.text var errors = await compositor.update() if len(errors) > 0: update_status(Status.ERROR, "\n".join(errors)) diff --git a/src/Filesystem.gd b/src/Filesystem.gd index 9f92261..2ead478 100644 --- a/src/Filesystem.gd +++ b/src/Filesystem.gd @@ -1,7 +1,7 @@ extends Node @onready var template_shader: Shader = load("res://src/shader/template.gdshader") -@onready var shader: Shader = template_shader.duplicate() +@onready var shader_code: String = template_shader.code var original_image: ImageTexture var additional_images: Dictionary @@ -13,7 +13,7 @@ var last_shader_savepath = "" var last_original_image_path = "" func reset(): - self.shader = self.template_shader.duplicate() + self.shader_code = self.template_shader.code self.last_image_savepath = "" self.last_shader_savepath = "" self.last_original_image_path = "" @@ -61,9 +61,7 @@ func save_result(path: String): func load_shader(path: String): print("Load ", path) var file = FileAccess.open(path, FileAccess.READ) - var shader_code = file.get_as_text() - self.shader = Shader.new() - shader.code = shader_code + 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 @@ -71,7 +69,7 @@ func load_shader(path: String): func save_shader(path: String): print("Save ", path) var file = FileAccess.open(path, FileAccess.WRITE) - file.store_string(self.shader.code) + file.store_string(self.shader_code) if "/" in path: # update current working directory self.cwd = path.substr(0, path.rfind("/")) self.last_shader_savepath = path diff --git a/src/ShaderDirectiveParser.gd b/src/ShaderDirectiveParser.gd index 6dd031b..f25af1c 100644 --- a/src/ShaderDirectiveParser.gd +++ b/src/ShaderDirectiveParser.gd @@ -1,16 +1,11 @@ extends Node -var _load_regex: RegEx -var _load_additional_regex: RegEx - -func _ready(): - self._load_regex = RegEx.new() - self._load_additional_regex = RegEx.new() - self._load_regex.compile(r'\/\/!load\s(.*)') - self._load_additional_regex.compile(r'\/\/!load\+\s(\w*)\s(.*)') +var _load_regex: RegEx = RegEx.create_from_string(r'\/\/!load\s(.*)') +var _load_additional_regex: RegEx = RegEx.create_from_string(r'\/\/!load\+\s(\w*)\s(.*)') +var _iterate_regex: RegEx = RegEx.create_from_string(r'\/\/!steps\s([0-9]+)\s*') func parse_load_directive(shader: Shader) -> PackedStringArray: - var regex_match = self._load_regex.search(Filesystem.shader.code) + var regex_match = self._load_regex.search(shader.code) if regex_match == null: return [] return regex_match.strings @@ -20,3 +15,10 @@ func parse_load_additional_directive(shader: Shader) -> Array[PackedStringArray] for m in self._load_additional_regex.search_all(shader.code): results.append(m.strings) return results + +func parse_steps_directive(shader: Shader) -> int: + var regex_match = self._iterate_regex.search(shader.code) + if regex_match == null: + return 1 + else: + return max(0, int(regex_match.strings[1]))