Skip to content

Commit

Permalink
Layer blend modes (#911)
Browse files Browse the repository at this point in the history
* Preview blend modes

No support for exporting and layer merging yet. Also need to fix the move tool preview.

* Preview blend modes on tile mode

* Raise layer limit to 1024

* Export images with layer blending modes

* Save blend modes in pxo files

* Merge layers with blending modes

* Fix crash when adding a new layer

* Preview blending in the other canvases

* Update DrawingAlgos.gd

* Move tool preview

* Re-arrange blend menu and add lighten, darken, linear burn and exclusion

* Add divide blend mode

* Add hue, saturation, color & luminosity blend modes

* Undo/redo when changing blend modes
  • Loading branch information
OverloadedOrama authored Oct 21, 2023
1 parent 6247ab2 commit 8de9697
Show file tree
Hide file tree
Showing 18 changed files with 448 additions and 169 deletions.
63 changes: 63 additions & 0 deletions src/Autoload/DrawingAlgos.gd
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,73 @@ extends Node
enum GradientDirection { TOP, BOTTOM, LEFT, RIGHT }
## Continuation from Image.Interpolation
enum Interpolation { SCALE3X = 5, CLEANEDGE = 6, OMNISCALE = 7 }
var blend_layers_shader := preload("res://src/Shaders/BlendLayers.gdshader")
var clean_edge_shader: Shader
var omniscale_shader := preload("res://src/Shaders/Rotation/OmniScale.gdshader")


## 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
if not Global.current_project.layers[i].is_visible_in_hierarchy():
continue
textures.append(current_cels[i].get_image())
opacities.append(current_cels[i].opacity)
blend_modes.append(Global.current_project.layers[i].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
) -> 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
if not project.layers[cel_ind].is_visible_in_hierarchy():
continue
var cel := frame.cels[cel_ind]
textures.append(cel.get_image())
opacities.append(cel.opacity)
blend_modes.append(Global.current_project.layers[cel_ind].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)


## Algorithm based on http://members.chello.at/easyfilter/bresenham.html
func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]:
var array: Array[Vector2i] = []
Expand Down
56 changes: 2 additions & 54 deletions src/Autoload/Export.gd
Original file line number Diff line number Diff line change
Expand Up @@ -484,9 +484,9 @@ func _blend_layers(
image: Image, frame: Frame, origin := Vector2i.ZERO, project := Global.current_project
) -> void:
if export_layers == 0:
blend_all_layers(image, frame, origin, project)
DrawingAlgos.blend_all_layers(image, frame, origin, project)
elif export_layers == 1:
blend_selected_cels(image, frame, origin, project)
DrawingAlgos.blend_selected_cels(image, frame, origin, project)
else:
var layer := project.layers[export_layers - 2]
var layer_image := Image.new()
Expand All @@ -497,57 +497,5 @@ func _blend_layers(
image.blend_rect(layer_image, Rect2i(Vector2i.ZERO, project.size), origin)


## 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 layer_i := 0
for cel in frame.cels:
if not project.layers[layer_i].is_visible_in_hierarchy():
layer_i += 1
continue
if cel is GroupCel:
layer_i += 1
continue
var cel_image := Image.new()
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
image.blend_rect(cel_image, Rect2i(Vector2i.ZERO, project.size), origin)
layer_i += 1


## 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
) -> void:
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
if not project.layers[cel_ind].is_visible_in_hierarchy():
continue
var cel: BaseCel = frame.cels[cel_ind]
var cel_image := Image.new()
cel_image.copy_from(cel.get_image())
if cel.opacity < 1: # If we have cel transparency
for xx in cel_image.get_size().x:
for yy in cel_image.get_size().y:
var pixel_color := cel_image.get_pixel(xx, yy)
var alpha: float = pixel_color.a * cel.opacity
cel_image.set_pixel(
xx, yy, Color(pixel_color.r, pixel_color.g, pixel_color.b, alpha)
)
image.blend_rect(cel_image, Rect2i(Vector2i.ZERO, project.size), origin)


func frames_divided_by_spritesheet_lines() -> int:
return ceili(number_of_frames / float(lines_count))
27 changes: 27 additions & 0 deletions src/Classes/BaseLayer.gd
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,34 @@ class_name BaseLayer
extends RefCounted
## Base class for layer properties. Different layer types extend from this class.

enum BlendModes {
NORMAL,
DARKEN,
MULTIPLY,
COLOR_BURN,
LINEAR_BURN,
LIGHTEN,
SCREEN,
COLOR_DODGE,
ADD,
OVERLAY,
SOFT_LIGHT,
HARD_LIGHT,
DIFFERENCE,
EXCLUSION,
SUBTRACT,
DIVIDE,
HUE,
SATURATION,
COLOR,
LUMINOSITY
}

var name := ""
var project: Project
var index: int
var parent: BaseLayer
var blend_mode := BlendModes.NORMAL
var visible := true
var locked := false
var new_cels_linked := false
Expand Down Expand Up @@ -130,6 +154,7 @@ func serialize() -> Dictionary:
"name": name,
"visible": visible,
"locked": locked,
"blend_mode": blend_mode,
"parent": parent.index if is_instance_valid(parent) else -1
}
if not cel_link_sets.is_empty():
Expand All @@ -148,6 +173,8 @@ func deserialize(dict: Dictionary) -> void:
name = dict.name
visible = dict.visible
locked = dict.locked
if dict.has("blend_mode"):
blend_mode = dict.blend_mode
if dict.get("parent", -1) != -1:
parent = project.layers[dict.parent]
if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility
Expand Down
4 changes: 2 additions & 2 deletions src/Classes/ImageEffect.gd
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,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))
Export.blend_selected_cels(selected_cels, frame)
DrawingAlgos.blend_selected_cels(selected_cels, frame)
current_frame.resize(Global.current_project.size.x, Global.current_project.size.y)
current_frame.fill(Color(0, 0, 0, 0))
Export.blend_all_layers(current_frame, frame)
DrawingAlgos.blend_all_layers(current_frame, frame)
update_preview()


Expand Down
3 changes: 2 additions & 1 deletion src/Classes/Project.gd
Original file line number Diff line number Diff line change
Expand Up @@ -522,8 +522,9 @@ func change_cel(new_frame: int, new_layer := -1) -> void:
toggle_layer_buttons()

if current_frame < frames.size(): # Set opacity slider
var cel_opacity: float = frames[current_frame].cels[current_layer].opacity
var cel_opacity := frames[current_frame].cels[current_layer].opacity
Global.layer_opacity_slider.value = cel_opacity * 100
Global.animation_timeline.blend_modes_button.selected = layers[current_layer].blend_mode
Global.canvas.queue_redraw()
Global.transparent_checker.update_rect()
Global.cel_changed.emit()
Expand Down
2 changes: 1 addition & 1 deletion src/Classes/ShaderImageEffect.gd
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func generate_image(img: Image, shader: Shader, params: Dictionary, size: Vector
RenderingServer.canvas_item_set_material(ci_rid, mat_rid)
for key in params:
var param = params[key]
if param is Texture2D:
if param is Texture2D or param is Texture2DArray:
RenderingServer.material_set_param(mat_rid, key, [param])
else:
RenderingServer.material_set_param(mat_rid, key, param)
Expand Down
146 changes: 146 additions & 0 deletions src/Shaders/BlendLayers.gdshader
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
shader_type canvas_item;
render_mode unshaded;

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;

// Conversion functions from
// https://gist.github.com/unitycoder/aaf94ddfe040ec2da93b58d3c65ab9d9
// licensed under MIT

// Converts from pure Hue to linear RGB
vec3 hue_to_rgb(float hue)
{
float R = abs(hue * 6.0 - 3.0) - 1.0;
float G = 2.0 - abs(hue * 6.0 - 2.0);
float B = 2.0 - abs(hue * 6.0 - 4.0);
return clamp(vec3(R, G, B), 0.0, 1.0);
}

// Converts from HSL to linear RGB
vec3 hsl_to_rgb(vec3 hsl)
{
vec3 rgb = hue_to_rgb(hsl.x);
float C = (1.0 - abs(2.0 * hsl.z - 1.0)) * hsl.y;
return (rgb - 0.5) * C + hsl.z;
}


// Converts a value from linear RGB to HCV (Hue, Chroma, Value)
vec3 rgb_to_hcv(vec3 rgb)
{
// Based on work by Sam Hocevar and Emil Persson
vec4 P = (rgb.g < rgb.b) ? vec4(rgb.bg, -1.0, 2.0/3.0) : vec4(rgb.gb, 0.0, -1.0/3.0);
vec4 Q = (rgb.r < P.x) ? vec4(P.xyw, rgb.r) : vec4(rgb.r, P.yzx);
float C = Q.x - min(Q.w, Q.y);
float H = abs((Q.w - Q.y) / (6.0 * C + HCV_EPSILON) + Q.z);
return vec3(H, C, Q.x);
}

// Converts from linear rgb to HSL
vec3 rgb_to_hsl(vec3 rgb)
{
vec3 HCV = rgb_to_hcv(rgb);
float L = HCV.z - HCV.y * 0.5;
float S = HCV.y / (1.0 - abs(L * 2.0 - 1.0) + HSL_EPSILON);
return vec3(HCV.x, S, L);
}


vec4 blend(int blend_type, vec4 current_color, vec4 prev_color, float opacity) {
vec4 result;
if (current_color.a <= 0.01) {
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!)
current_color.a = current_color.a * opacity; // Combine the layer opacity
switch(blend_type) {
case 1: // Darken
result.rgb = min(prev_color.rgb, current_color.rgb);
break;
case 2: // Multiply
result.rgb = prev_color.rgb * current_color.rgb;
break;
case 3: // Color burn
result.rgb = 1.0 - (1.0 - prev_color.rgb) / current_color.rgb;
break;
case 4: // Linear burn
result.rgb = prev_color.rgb + current_color.rgb - 1.0;
break;
case 5: // Lighten
result.rgb = max(prev_color.rgb, current_color.rgb);
break;
case 6: // Screen
result.rgb = mix(prev_color.rgb, 1.0 - (1.0 - prev_color.rgb) * (1.0 - current_color.rgb), current_color.a);
break;
case 7: // Color dodge
result.rgb = prev_color.rgb / (1.0 - current_color.rgb);
break;
case 8: // Add (linear dodge)
result.rgb = prev_color.rgb + current_color.rgb;
break;
case 9: // Overlay
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb, 1.0 - 2.0 * (1.0 - current_color.rgb) * (1.0 - prev_color.rgb), round(prev_color.rgb));
break;
case 10: // Soft light
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb + prev_color.rgb * prev_color.rgb * (1.0 - 2.0 * current_color.rgb), sqrt(prev_color.rgb) * (2.0 * current_color.rgb - 1.0) + (2.0 * prev_color.rgb) * (1.0 - current_color.rgb), round(prev_color.rgb));
break;
case 11: // Hard light
result.rgb = mix(2.0 * prev_color.rgb * current_color.rgb, 1.0 - 2.0 * (1.0 - current_color.rgb) * (1.0 - prev_color.rgb), round(current_color.rgb));
break;
case 12: // Difference
result.rgb = abs(prev_color.rgb - current_color.rgb);
break;
case 13: // Exclusion
result.rgb = prev_color.rgb + current_color.rgb - 2.0 * prev_color.rgb * current_color.rgb;
break;
case 14: // Subtract
result.rgb = prev_color.rgb - current_color.rgb;
break;
case 15: // Divide
result.rgb = prev_color.rgb / current_color.rgb;
break;
case 16: // Hue
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(current_hsl.r, prev_hsl.g, prev_hsl.b));
break;
case 17: // Saturation
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(prev_hsl.r, current_hsl.g, prev_hsl.b));
break;
case 18: // Color
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(current_hsl.r, current_hsl.g, prev_hsl.b));
break;
case 19: // Luminosity
vec3 current_hsl = rgb_to_hsl(current_color.rgb);
vec3 prev_hsl = rgb_to_hsl(prev_color.rgb);
result.rgb = hsl_to_rgb(vec3(prev_hsl.r, prev_hsl.g, current_hsl.b));
break;
default: // Normal (case 0)
result.rgb = prev_color.rgb * (1.0 - current_color.a) + current_color.rgb;
break;
}
result.a = prev_color.a * (1.0 - current_color.a) + current_color.a;
result = clamp(result, 0.0, 1.0);
return mix(current_color, result, prev_color.a);
}


void fragment() {
vec4 col = texture(layers, vec3(UV - origins[0], 0.0));
col.a *= opacities[0];
for(int i = 1; i < textureSize(layers, 0).z; i++) // Loops through every layer
{
col = blend(blend_modes[i], texture(layers, vec3(UV - origins[i], float(i))), col, opacities[i]);
}
COLOR = col;
}
Loading

0 comments on commit 8de9697

Please sign in to comment.