From bc8a9de4db916406de4f3c10c3c8d2ddda987596 Mon Sep 17 00:00:00 2001 From: Emmanouil Papadeas Date: Sat, 25 Nov 2023 00:10:19 +0200 Subject: [PATCH] Optimize layer blending modes and make them work on all GPUs (hopefully) - Fixes #938 --- src/Autoload/DrawingAlgos.gd | 75 ++++++---------- src/Autoload/Export.gd | 4 +- src/Classes/ImageEffect.gd | 4 +- src/Classes/Project.gd | 1 + src/Shaders/BlendLayers.gdshader | 41 +++++++-- src/UI/Canvas/Canvas.gd | 99 ++++++++++++++++----- src/UI/Canvas/Canvas.tscn | 5 +- src/UI/Canvas/CanvasPreview.gd | 33 +++---- src/UI/Canvas/Selection.gd | 4 +- src/UI/Dialogs/ImageEffects/ShaderEffect.gd | 2 +- src/UI/Dialogs/TileModeOffsetsDialog.gd | 2 +- src/UI/Recorder/Recorder.gd | 2 +- src/UI/Timeline/AnimationTimeline.gd | 14 ++- src/UI/Timeline/LayerButton.gd | 1 + 14 files changed, 170 insertions(+), 117 deletions(-) diff --git a/src/Autoload/DrawingAlgos.gd b/src/Autoload/DrawingAlgos.gd index 01d59d914..8c550e67c 100644 --- a/src/Autoload/DrawingAlgos.gd +++ b/src/Autoload/DrawingAlgos.gd @@ -9,64 +9,39 @@ var omniscale_shader := preload("res://src/Shaders/Effects/Rotation/OmniScale.gd ## Blends canvas layers into passed image starting from the origin position -func blend_all_layers( - image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project -) -> void: - var current_cels := frame.cels - var textures: Array[Image] = [] - var opacities := PackedFloat32Array() - var blend_modes := PackedInt32Array() - - for i in Global.current_project.layers.size(): - if current_cels[i] is GroupCel: - continue - var layer := Global.current_project.layers[i] - if not layer.is_visible_in_hierarchy(): - continue - var cel_image := layer.display_effects(current_cels[i]) - textures.append(cel_image) - opacities.append(current_cels[i].opacity) - blend_modes.append(layer.blend_mode) - var texture_array := Texture2DArray.new() - texture_array.create_from_images(textures) - var params := { - "layers": texture_array, - "opacities": opacities, - "blend_modes": blend_modes, - } - var blended := Image.create(project.size.x, project.size.y, false, image.get_format()) - var gen := ShaderImageEffect.new() - gen.generate_image(blended, blend_layers_shader, params, project.size) - image.blend_rect(blended, Rect2i(Vector2i.ZERO, project.size), origin) - - -## Blends selected cels of the given frame into passed image starting from the origin position -func blend_selected_cels( - image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project +func blend_layers( + image: Image, + frame: Frame, + origin := Vector2i.ZERO, + project := Global.current_project, + only_selected := false ) -> void: var textures: Array[Image] = [] - var opacities := PackedFloat32Array() - var blend_modes := PackedInt32Array() - for cel_ind in frame.cels.size(): - var test_array := [project.current_frame, cel_ind] - if not test_array in project.selected_cels: - continue - if frame.cels[cel_ind] is GroupCel: - continue - var layer := project.layers[cel_ind] - if not layer.is_visible_in_hierarchy(): - continue - var cel := frame.cels[cel_ind] + # Nx3 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities and the third are the origins + var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) + for i in project.layers.size(): + var layer := project.layers[i] + var include := true if layer.is_visible_in_hierarchy() else false + if only_selected and include: + var test_array := [project.frames.find(frame), i] + if not test_array in project.selected_cels: + include = false + var cel := frame.cels[i] var cel_image := layer.display_effects(cel) textures.append(cel_image) - opacities.append(cel.opacity) - blend_modes.append(layer.blend_mode) + # Store the blend mode + metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) + # Store the opacity + if include: + metadata_image.set_pixel(i, 1, Color(cel.opacity, 0.0, 0.0, 0.0)) + else: + metadata_image.set_pixel(i, 1, Color()) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) var params := { "layers": texture_array, - "opacities": opacities, - "blend_modes": blend_modes, + "metadata": ImageTexture.create_from_image(metadata_image), } var blended := Image.create(project.size.x, project.size.y, false, image.get_format()) var gen := ShaderImageEffect.new() diff --git a/src/Autoload/Export.gd b/src/Autoload/Export.gd index f3aed5a8e..de28b3442 100644 --- a/src/Autoload/Export.gd +++ b/src/Autoload/Export.gd @@ -484,9 +484,9 @@ func _blend_layers( image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project ) -> void: if export_layers == 0: - DrawingAlgos.blend_all_layers(image, frame, origin, project) + DrawingAlgos.blend_layers(image, frame, origin, project) elif export_layers == 1: - DrawingAlgos.blend_selected_cels(image, frame, origin, project) + DrawingAlgos.blend_layers(image, frame, origin, project, true) else: var layer := project.layers[export_layers - 2] var layer_image := Image.new() diff --git a/src/Classes/ImageEffect.gd b/src/Classes/ImageEffect.gd index 4913fb918..0cb8c6c5d 100644 --- a/src/Classes/ImageEffect.gd +++ b/src/Classes/ImageEffect.gd @@ -196,10 +196,10 @@ func set_and_update_preview_image(frame_idx: int) -> void: var frame := Global.current_project.frames[frame_idx] selected_cels.resize(Global.current_project.size.x, Global.current_project.size.y) selected_cels.fill(Color(0, 0, 0, 0)) - DrawingAlgos.blend_selected_cels(selected_cels, frame) + DrawingAlgos.blend_layers(selected_cels, frame, Vector2i.ZERO, Global.current_project, true) current_frame.resize(Global.current_project.size.x, Global.current_project.size.y) current_frame.fill(Color(0, 0, 0, 0)) - DrawingAlgos.blend_all_layers(current_frame, frame) + DrawingAlgos.blend_layers(current_frame, frame) update_preview() diff --git a/src/Classes/Project.gd b/src/Classes/Project.gd index 0c6bd175b..a4197bba4 100644 --- a/src/Classes/Project.gd +++ b/src/Classes/Project.gd @@ -538,6 +538,7 @@ func change_cel(new_frame: int, new_layer := -1) -> void: if get_current_cel() is Cel3D: await RenderingServer.frame_post_draw await RenderingServer.frame_post_draw + Global.canvas.update_all_layers = true Global.canvas.queue_redraw() diff --git a/src/Shaders/BlendLayers.gdshader b/src/Shaders/BlendLayers.gdshader index 2e6f4626f..2ca9501d6 100644 --- a/src/Shaders/BlendLayers.gdshader +++ b/src/Shaders/BlendLayers.gdshader @@ -5,9 +5,11 @@ const float HCV_EPSILON = 1e-10; const float HSL_EPSILON = 1e-10; uniform sampler2DArray layers : filter_nearest; -uniform float[1024] opacities; -uniform int[1024] blend_modes; -uniform vec2[1024] origins; +// Nx3 texture, where N is the number of layers and the first row are the blend modes, +// the second are the opacities and the third are the origins +uniform sampler2D metadata : filter_nearest; +uniform bool origin_x_positive = true; +uniform bool origin_y_positive = true; // Conversion functions from // https://gist.github.com/unitycoder/aaf94ddfe040ec2da93b58d3c65ab9d9 @@ -54,7 +56,7 @@ vec3 rgb_to_hsl(vec3 rgb) vec4 blend(int blend_type, vec4 current_color, vec4 prev_color, float opacity) { vec4 result; - if (current_color.a <= 0.01) { + if (current_color.a <= 0.001 || opacity <= 0.001) { return prev_color; } current_color.rgb = current_color.rgb * opacity; // Premultiply with the layer texture's alpha to prevent semi transparent pixels from being too bright (ALL LAYER TYPES!) @@ -142,15 +144,36 @@ float border_trim(vec4 color, vec2 uv) { void fragment() { - vec4 col = texture(layers, vec3(UV - origins[0], 0.0)); - col.a = border_trim(col, UV - origins[0]); - col.a *= opacities[0]; + ivec2 metadata_size = textureSize(metadata, 0) - 1; + vec2 first_origin = texture(metadata, vec2(0.0, 2.0 / float(metadata_size.y))).rg; + if (!origin_x_positive) { + first_origin.x = -first_origin.x; + } + if (!origin_y_positive) { + first_origin.y = -first_origin.y; + } + float first_opacity = texture(metadata, vec2(0.0, 1.0 / float(metadata_size.y))).r; + vec4 col = texture(layers, vec3(UV - first_origin, 0.0)); + col.a = border_trim(col, UV - first_origin); + col.a *= first_opacity; for(int i = 1; i < textureSize(layers, 0).z; i++) // Loops through every layer { - vec2 uv = UV - origins[i]; + float blend_mode_float = texture(metadata, vec2(float(i) / float(metadata_size.x), 0.0)).r; + // Blend modes are being stored as integers divided by 255, so convert them back to + // their integer form + int current_blend_mode = int(floor(blend_mode_float * 255.0)); + vec2 current_origin = texture(metadata, vec2(float(i) / float(metadata_size.x), 2.0 / float(metadata_size.y))).rg; + if (!origin_x_positive) { + current_origin.x = -current_origin.x; + } + if (!origin_y_positive) { + current_origin.y = -current_origin.y; + } + float current_opacity = texture(metadata, vec2(float(i) / float(metadata_size.x), 1.0 / float(metadata_size.y))).r; + vec2 uv = UV - current_origin; vec4 texture_color = texture(layers, vec3(uv, float(i))); texture_color.a = border_trim(texture_color, uv); - col = blend(blend_modes[i], texture_color, col, opacities[i]); + col = blend(current_blend_mode, texture_color, col, current_opacity); } COLOR = col; } diff --git a/src/UI/Canvas/Canvas.gd b/src/UI/Canvas/Canvas.gd index 4d08799a5..89f560190 100644 --- a/src/UI/Canvas/Canvas.gd +++ b/src/UI/Canvas/Canvas.gd @@ -6,7 +6,11 @@ const CURSOR_SPEED_RATE := 6.0 var current_pixel := Vector2.ZERO var sprite_changed_this_frame := false ## For optimization purposes +var update_all_layers := false var move_preview_location := Vector2i.ZERO +var layer_texture_array := Texture2DArray.new() +var layer_metadata_image := Image.new() +var layer_metadata_texture := ImageTexture.new() @onready var currently_visible_frame := $CurrentlyVisibleFrame as SubViewport @onready var current_frame_drawer := $CurrentlyVisibleFrame/CurrentFrameDrawer as Node2D @@ -108,6 +112,19 @@ func update_texture(layer_i: int, frame_i := -1, project := Global.current_proje if frame_i < project.frames.size() and layer_i < project.layers.size(): var current_cel := project.frames[frame_i].cels[layer_i] current_cel.update_texture() + # Needed so that changes happening to the non-selected layer(s) are also visible + # e.g. when undoing/redoing, when applying image effects to the entire frame, etc + var layer := project.layers[layer_i] + var cel_image: Image + if Global.display_layer_effects: + cel_image = layer.display_effects(current_cel) + else: + cel_image = current_cel.get_image() + if ( + cel_image.get_size() + == Vector2i(layer_texture_array.get_width(), layer_texture_array.get_height()) + ): + layer_texture_array.update_layer(cel_image, layer_i) func update_selected_cels_textures(project := Global.current_project) -> void: @@ -120,36 +137,72 @@ func update_selected_cels_textures(project := Global.current_project) -> void: func draw_layers() -> void: - var current_frame := Global.current_project.frames[Global.current_project.current_frame] - var current_cels := current_frame.cels - var textures: Array[Image] = [] - var opacities := PackedFloat32Array() - var blend_modes := PackedInt32Array() - var origins := PackedVector2Array() - # Draw current frame layers - for i in Global.current_project.layers.size(): - if current_cels[i] is GroupCel: - continue - var layer := Global.current_project.layers[i] - if layer.is_visible_in_hierarchy(): + var project := Global.current_project + var current_cels := project.frames[project.current_frame].cels + var recreate_texture_array := ( + layer_texture_array.get_layers() != project.layers.size() + or layer_texture_array.get_width() != project.size.x + or layer_texture_array.get_height() != project.size.y + ) + if recreate_texture_array: + var textures: Array[Image] = [] + # Nx3 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities and the third are the origins + layer_metadata_image = Image.create(project.layers.size(), 3, false, Image.FORMAT_RG8) + # Draw current frame layers + for i in project.layers.size(): + var layer := project.layers[i] var cel_image: Image if Global.display_layer_effects: cel_image = layer.display_effects(current_cels[i]) else: cel_image = current_cels[i].get_image() textures.append(cel_image) - opacities.append(current_cels[i].opacity) - if [Global.current_project.current_frame, i] in Global.current_project.selected_cels: - origins.append(Vector2(move_preview_location) / Vector2(cel_image.get_size())) + # Store the blend mode + layer_metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) + # Store the opacity + if layer.is_visible_in_hierarchy(): + layer_metadata_image.set_pixel(i, 1, Color(current_cels[i].opacity, 0.0, 0.0, 0.0)) else: - origins.append(Vector2.ZERO) - blend_modes.append(layer.blend_mode) - var texture_array := Texture2DArray.new() - texture_array.create_from_images(textures) - material.set_shader_parameter("layers", texture_array) - material.set_shader_parameter("opacities", opacities) - material.set_shader_parameter("blend_modes", blend_modes) - material.set_shader_parameter("origins", origins) + layer_metadata_image.set_pixel(i, 1, Color()) + # Store the origin + if [project.current_frame, i] in project.selected_cels: + var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) + layer_metadata_image.set_pixel(i, 2, Color(origin.x, origin.y, 0.0, 0.0)) + else: + layer_metadata_image.set_pixel(i, 2, Color()) + + layer_texture_array.create_from_images(textures) + layer_metadata_texture.set_image(layer_metadata_image) + else: # Update the TextureArray + if layer_texture_array.get_layers() > 0: + for i in project.layers.size(): + var layer := project.layers[i] + var test_array := [project.current_frame, i] + if not update_all_layers: + if not test_array in project.selected_cels: + continue + var cel := current_cels[i] + var cel_image: Image + if Global.display_layer_effects: + cel_image = layer.display_effects(cel) + else: + cel_image = cel.get_image() + layer_texture_array.update_layer(cel_image, i) + layer_metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) + if layer.is_visible_in_hierarchy(): + layer_metadata_image.set_pixel(i, 1, Color(cel.opacity, 0.0, 0.0, 0.0)) + else: + layer_metadata_image.set_pixel(i, 1, Color()) + var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) + layer_metadata_image.set_pixel(i, 2, Color(origin.x, origin.y, 0.0, 0.0)) + layer_metadata_texture.update(layer_metadata_image) + + material.set_shader_parameter("layers", layer_texture_array) + material.set_shader_parameter("metadata", layer_metadata_texture) + material.set_shader_parameter("origin_x_positive", move_preview_location.x > 0) + material.set_shader_parameter("origin_y_positive", move_preview_location.y > 0) + update_all_layers = false func refresh_onion() -> void: diff --git a/src/UI/Canvas/Canvas.tscn b/src/UI/Canvas/Canvas.tscn index 642d45f7a..276703040 100644 --- a/src/UI/Canvas/Canvas.tscn +++ b/src/UI/Canvas/Canvas.tscn @@ -19,9 +19,8 @@ [sub_resource type="ShaderMaterial" id="ShaderMaterial_6b0ox"] shader = ExtResource("1_253dh") -shader_parameter/opacities = null -shader_parameter/blend_modes = null -shader_parameter/origins = null +shader_parameter/origin_x_positive = true +shader_parameter/origin_y_positive = true [sub_resource type="CanvasItemMaterial" id="1"] blend_mode = 4 diff --git a/src/UI/Canvas/CanvasPreview.gd b/src/UI/Canvas/CanvasPreview.gd index 05e886037..63d7ca7c4 100644 --- a/src/UI/Canvas/CanvasPreview.gd +++ b/src/UI/Canvas/CanvasPreview.gd @@ -72,30 +72,33 @@ func _draw() -> void: func _draw_layers() -> void: - var current_frame := Global.current_project.frames[frame_index] + var project := Global.current_project + var current_frame := project.frames[frame_index] var current_cels := current_frame.cels var textures: Array[Image] = [] - var opacities := PackedFloat32Array() - var blend_modes := PackedInt32Array() + # Nx3 texture, where N is the number of layers and the first row are the blend modes, + # the second are the opacities and the third are the origins + var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) # Draw current frame layers - for i in Global.current_project.layers.size(): + for i in project.layers.size(): if current_cels[i] is GroupCel: continue - var layer := Global.current_project.layers[i] + var layer := project.layers[i] + var cel_image: Image + if Global.display_layer_effects: + cel_image = layer.display_effects(current_cels[i]) + else: + cel_image = current_cels[i].get_image() + textures.append(cel_image) + metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) if layer.is_visible_in_hierarchy(): - var cel_image: Image - if Global.display_layer_effects: - cel_image = layer.display_effects(current_cels[i]) - else: - cel_image = current_cels[i].get_image() - textures.append(cel_image) - opacities.append(current_cels[i].opacity) - blend_modes.append(layer.blend_mode) + metadata_image.set_pixel(i, 1, Color(current_cels[i].opacity, 0.0, 0.0, 0.0)) + else: + metadata_image.set_pixel(i, 1, Color(0.0, 0.0, 0.0, 0.0)) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) material.set_shader_parameter("layers", texture_array) - material.set_shader_parameter("opacities", opacities) - material.set_shader_parameter("blend_modes", blend_modes) + material.set_shader_parameter("metadata", ImageTexture.create_from_image(metadata_image)) func _on_AnimationTimer_timeout() -> void: diff --git a/src/UI/Canvas/Selection.gd b/src/UI/Canvas/Selection.gd index 31fc7e39b..0b5683280 100644 --- a/src/UI/Canvas/Selection.gd +++ b/src/UI/Canvas/Selection.gd @@ -878,7 +878,9 @@ func clear_selection(use_undo := false) -> void: func _get_preview_image() -> void: var project := Global.current_project var blended_image := Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) - DrawingAlgos.blend_selected_cels(blended_image, project.frames[project.current_frame]) + DrawingAlgos.blend_layers( + blended_image, project.frames[project.current_frame], Vector2i.ZERO, project, true + ) if original_preview_image.is_empty(): original_preview_image = Image.create( big_bounding_rectangle.size.x, big_bounding_rectangle.size.y, false, Image.FORMAT_RGBA8 diff --git a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd index 020a5a2fb..5b44531a1 100644 --- a/src/UI/Dialogs/ImageEffects/ShaderEffect.gd +++ b/src/UI/Dialogs/ImageEffects/ShaderEffect.gd @@ -10,7 +10,7 @@ var params := {} func _about_to_popup() -> void: Global.canvas.selection.transform_content_confirm() var frame := Global.current_project.frames[Global.current_project.current_frame] - DrawingAlgos.blend_selected_cels(selected_cels, frame) + DrawingAlgos.blend_layers(selected_cels, frame, Vector2i.ZERO, Global.current_project, true) preview_image.copy_from(selected_cels) preview.texture = ImageTexture.create_from_image(preview_image) diff --git a/src/UI/Dialogs/TileModeOffsetsDialog.gd b/src/UI/Dialogs/TileModeOffsetsDialog.gd index 273ed5890..61dc4a472 100644 --- a/src/UI/Dialogs/TileModeOffsetsDialog.gd +++ b/src/UI/Dialogs/TileModeOffsetsDialog.gd @@ -125,7 +125,7 @@ func change_mask(): var tiles := Global.current_project.tiles var tiles_size := tiles.tile_size var image := Image.create(tiles_size.x, tiles_size.y, false, Image.FORMAT_RGBA8) - DrawingAlgos.blend_all_layers(image, current_frame) + DrawingAlgos.blend_layers(image, current_frame) if ( image.get_used_rect().size == Vector2i.ZERO or not $VBoxContainer/HBoxContainer/Masking.button_pressed diff --git a/src/UI/Recorder/Recorder.gd b/src/UI/Recorder/Recorder.gd index d021a432e..f1e555e01 100644 --- a/src/UI/Recorder/Recorder.gd +++ b/src/UI/Recorder/Recorder.gd @@ -74,7 +74,7 @@ func capture_frame() -> void: else: var frame := project.frames[project.current_frame] image = Image.create(project.size.x, project.size.y, false, Image.FORMAT_RGBA8) - DrawingAlgos.blend_all_layers(image, frame, Vector2i.ZERO, project) + DrawingAlgos.blend_layers(image, frame, Vector2i.ZERO, project) if mode == Mode.CANVAS: if resize != 100: diff --git a/src/UI/Timeline/AnimationTimeline.gd b/src/UI/Timeline/AnimationTimeline.gd index 218e62a19..0553b8c95 100644 --- a/src/UI/Timeline/AnimationTimeline.gd +++ b/src/UI/Timeline/AnimationTimeline.gd @@ -917,20 +917,16 @@ func _on_MergeDownLayer_pressed() -> void: var top_image := top_layer.display_effects(top_cel) var bottom_cel := frame.cels[bottom_layer.index] var textures: Array[Image] = [] - var opacities := PackedFloat32Array() - var blend_modes := PackedInt32Array() + var metadata_image := Image.create(2, 3, false, Image.FORMAT_R8) textures.append(bottom_cel.get_image()) - opacities.append(bottom_cel.opacity) - blend_modes.append(bottom_layer.blend_mode) + metadata_image.set_pixel(0, 1, Color(1.0, 0.0, 0.0, 0.0)) textures.append(top_image) - opacities.append(frame.cels[top_layer.index].opacity) - blend_modes.append(top_layer.blend_mode) + metadata_image.set_pixel(1, 0, Color(top_layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) + metadata_image.set_pixel(1, 1, Color(frame.cels[top_layer.index].opacity, 0.0, 0.0, 0.0)) var texture_array := Texture2DArray.new() texture_array.create_from_images(textures) var params := { - "layers": texture_array, - "opacities": opacities, - "blend_modes": blend_modes, + "layers": texture_array, "metadata": ImageTexture.create_from_image(metadata_image) } var bottom_image := Image.create( top_image.get_width(), top_image.get_height(), false, top_image.get_format() diff --git a/src/UI/Timeline/LayerButton.gd b/src/UI/Timeline/LayerButton.gd index edfba951c..8b9bea59f 100644 --- a/src/UI/Timeline/LayerButton.gd +++ b/src/UI/Timeline/LayerButton.gd @@ -155,6 +155,7 @@ func _on_ExpandButton_pressed() -> void: func _on_VisibilityButton_pressed() -> void: Global.canvas.selection.transform_content_confirm() Global.current_project.layers[layer].visible = !Global.current_project.layers[layer].visible + Global.canvas.update_all_layers = true Global.canvas.queue_redraw() if Global.select_layer_on_button_click: _select_current_layer()