1
0
Fork 0
mirror of https://github.com/Orama-Interactive/Pixelorama.git synced 2025-01-18 09:09:47 +00:00

Implement basic clipping masks

A very simple implementation, not as complex as something like #768 yet, but it can be done in the future.

The main current limitation is that it doesn't work with group layers as of right now.
This commit is contained in:
Emmanouil Papadeas 2024-03-14 01:08:57 +02:00
parent fdc92ccfc3
commit c1b78e4c01
10 changed files with 131 additions and 63 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ieo8fsapcgsy"
path="res://.godot/imported/clipping_mask.png-735677b4fff2e062e79993566d07bdd3.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/graphics/layers/clipping_mask.png"
dest_files=["res://.godot/imported/clipping_mask.png-735677b4fff2e062e79993566d07bdd3.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
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/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

View file

@ -26,9 +26,10 @@ func blend_layers(
only_selected_layers := false, only_selected_layers := false,
) -> void: ) -> void:
var textures: Array[Image] = [] var textures: Array[Image] = []
# Nx3 texture, where N is the number of layers and the first row are the blend modes, # Nx4 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 # the second are the opacities, the third are the origins and the fourth are the
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) # clipping mask booleans.
var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8)
var frame_index := project.frames.find(frame) var frame_index := project.frames.find(frame)
var previous_ordered_layers: Array[int] = project.ordered_layers var previous_ordered_layers: Array[int] = project.ordered_layers
project.order_layers(frame_index) project.order_layers(frame_index)
@ -51,14 +52,7 @@ func blend_layers(
var cel := frame.cels[ordered_index] var cel := frame.cels[ordered_index]
var cel_image := layer.display_effects(cel) var cel_image := layer.display_effects(cel)
textures.append(cel_image) textures.append(cel_image)
# Store the blend mode set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include)
metadata_image.set_pixel(ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
# Store the opacity
if include:
var opacity := cel.get_final_opacity(layer)
metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0))
else:
metadata_image.set_pixel(ordered_index, 1, Color())
var texture_array := Texture2DArray.new() var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures) texture_array.create_from_images(textures)
var params := { var params := {
@ -73,6 +67,24 @@ func blend_layers(
project.ordered_layers = previous_ordered_layers project.ordered_layers = previous_ordered_layers
func set_layer_metadata_image(
layer: BaseLayer, cel: BaseCel, image: Image, index: int, include := true
) -> void:
# Store the blend mode
image.set_pixel(index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
# Store the opacity
if layer.is_visible_in_hierarchy() and include:
var opacity := cel.get_final_opacity(layer)
image.set_pixel(index, 1, Color(opacity, 0.0, 0.0, 0.0))
else:
image.set_pixel(index, 1, Color())
# Store the clipping mask boolean
if layer.clipping_mask:
image.set_pixel(index, 3, Color.WHITE)
else:
image.set_pixel(index, 3, Color.BLACK)
## Algorithm based on http://members.chello.at/easyfilter/bresenham.html ## Algorithm based on http://members.chello.at/easyfilter/bresenham.html
func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]: func get_ellipse_points(pos: Vector2i, size: Vector2i) -> Array[Vector2i]:
var array: Array[Vector2i] = [] var array: Array[Vector2i] = []

View file

@ -36,6 +36,7 @@ var visible := true ## Sets visibility of the layer.
var locked := false ## Images of a locked layer won't be overritten. var locked := false ## Images of a locked layer won't be overritten.
var new_cels_linked := false ## Determines if new cel of the layer should be linked or not. var new_cels_linked := false ## Determines if new cel of the layer should be linked or not.
var blend_mode := BlendModes.NORMAL ## Blend mode of the current layer. var blend_mode := BlendModes.NORMAL ## Blend mode of the current layer.
var clipping_mask := false ## If [code]true[/code], the layer acts as a clipping mask.
var opacity := 1.0 ## The opacity of the layer, affects all frames that belong to that layer. var opacity := 1.0 ## The opacity of the layer, affects all frames that belong to that layer.
var cel_link_sets: Array[Dictionary] = [] ## Each Dictionary represents a cel's "link set" var cel_link_sets: Array[Dictionary] = [] ## Each Dictionary represents a cel's "link set"
var effects: Array[LayerEffect] ## An array for non-destructive effects of the layer. var effects: Array[LayerEffect] ## An array for non-destructive effects of the layer.
@ -215,6 +216,7 @@ func serialize() -> Dictionary:
"visible": visible, "visible": visible,
"locked": locked, "locked": locked,
"blend_mode": blend_mode, "blend_mode": blend_mode,
"clipping_mask": clipping_mask,
"opacity": opacity, "opacity": opacity,
"parent": parent.index if is_instance_valid(parent) else -1, "parent": parent.index if is_instance_valid(parent) else -1,
"effects": effect_data "effects": effect_data
@ -233,13 +235,12 @@ func serialize() -> Dictionary:
## Sets the layer data according to a curated [Dictionary] obtained from [method serialize]. ## Sets the layer data according to a curated [Dictionary] obtained from [method serialize].
func deserialize(dict: Dictionary) -> void: func deserialize(dict: Dictionary) -> void:
name = dict.name name = dict.get("name", "")
visible = dict.visible visible = dict.get("visible", true)
locked = dict.locked locked = dict.get("locked", false)
if dict.has("blend_mode"): blend_mode = dict.get("blend_mode", BlendModes.NORMAL)
blend_mode = dict.blend_mode clipping_mask = dict.get("clipping_mask", false)
if dict.has("opacity"): opacity = dict.get("opacity", 1.0)
opacity = dict.opacity
if dict.get("parent", -1) != -1: if dict.get("parent", -1) != -1:
parent = project.layers[dict.parent] parent = project.layers[dict.parent]
if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility if dict.has("linked_cels") and not dict["linked_cels"].is_empty(): # Backwards compatibility

View file

@ -5,8 +5,9 @@ const float HCV_EPSILON = 1e-10;
const float HSL_EPSILON = 1e-10; const float HSL_EPSILON = 1e-10;
uniform sampler2DArray layers : filter_nearest; uniform sampler2DArray layers : filter_nearest;
// Nx3 texture, where N is the number of layers and the first row are the blend modes, // Nx4 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 // the second are the opacities, the third are the origins and the fourth are the
// clipping mask booleans.
uniform sampler2D metadata : filter_nearest; uniform sampler2D metadata : filter_nearest;
uniform bool origin_x_positive = true; uniform bool origin_x_positive = true;
uniform bool origin_y_positive = true; uniform bool origin_y_positive = true;
@ -153,9 +154,9 @@ void fragment() {
first_origin.y = -first_origin.y; first_origin.y = -first_origin.y;
} }
float first_opacity = texture(metadata, vec2(0.0, 1.0 / float(metadata_size.y))).r; 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)); vec4 result_color = texture(layers, vec3(UV - first_origin, 0.0));
col.a = border_trim(col, UV - first_origin); result_color.a = border_trim(result_color, UV - first_origin);
col.a *= first_opacity; result_color.a *= first_opacity;
for(int i = 1; i < metadata_size.x + 1; i++) // Loops through every layer for(int i = 1; i < metadata_size.x + 1; i++) // Loops through every layer
{ {
float blend_mode_float = texture(metadata, vec2(float(i) / float(metadata_size.x), 0.0)).r; float blend_mode_float = texture(metadata, vec2(float(i) / float(metadata_size.x), 0.0)).r;
@ -171,9 +172,12 @@ void fragment() {
} }
float current_opacity = texture(metadata, vec2(float(i) / float(metadata_size.x), 1.0 / float(metadata_size.y))).r; float current_opacity = texture(metadata, vec2(float(i) / float(metadata_size.x), 1.0 / float(metadata_size.y))).r;
vec2 uv = UV - current_origin; vec2 uv = UV - current_origin;
vec4 texture_color = texture(layers, vec3(uv, float(i))); vec4 layer_color = texture(layers, vec3(uv, float(i)));
texture_color.a = border_trim(texture_color, uv); vec4 prev_layer_color = texture(layers, vec3(uv, float(i - 1)));
col = blend(current_blend_mode, texture_color, col, current_opacity); float clipping_mask = texture(metadata, vec2(float(i) / float(metadata_size.x), 3.0 / float(metadata_size.y))).r;
layer_color.a *= prev_layer_color.a * step(0.5, clipping_mask) + 1.0 * step(clipping_mask, 0.5);
layer_color.a = border_trim(layer_color, uv);
result_color = blend(current_blend_mode, layer_color, result_color, current_opacity);
} }
COLOR = col; COLOR = result_color;
} }

View file

@ -148,9 +148,10 @@ func draw_layers() -> void:
if recreate_texture_array: if recreate_texture_array:
var textures: Array[Image] = [] var textures: Array[Image] = []
textures.resize(project.layers.size()) textures.resize(project.layers.size())
# Nx3 texture, where N is the number of layers and the first row are the blend modes, # Nx4 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 # the second are the opacities, the third are the origins and the fourth are the
layer_metadata_image = Image.create(project.layers.size(), 3, false, Image.FORMAT_RG8) # clipping mask booleans.
layer_metadata_image = Image.create(project.layers.size(), 4, false, Image.FORMAT_RG8)
# Draw current frame layers # Draw current frame layers
for i in project.layers.size(): for i in project.layers.size():
var ordered_index := project.ordered_layers[i] var ordered_index := project.ordered_layers[i]
@ -162,16 +163,7 @@ func draw_layers() -> void:
else: else:
cel_image = cel.get_image() cel_image = cel.get_image()
textures[ordered_index] = cel_image textures[ordered_index] = cel_image
# Store the blend mode DrawingAlgos.set_layer_metadata_image(layer, cel, layer_metadata_image, ordered_index)
layer_metadata_image.set_pixel(
ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)
)
# Store the opacity
if layer.is_visible_in_hierarchy():
var opacity := cel.get_final_opacity(layer)
layer_metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0))
else:
layer_metadata_image.set_pixel(ordered_index, 1, Color())
# Store the origin # Store the origin
if [project.current_frame, i] in project.selected_cels: if [project.current_frame, i] in project.selected_cels:
var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
@ -199,14 +191,10 @@ func draw_layers() -> void:
else: else:
cel_image = cel.get_image() cel_image = cel.get_image()
layer_texture_array.update_layer(cel_image, ordered_index) layer_texture_array.update_layer(cel_image, ordered_index)
layer_metadata_image.set_pixel( DrawingAlgos.set_layer_metadata_image(
ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0) layer, cel, layer_metadata_image, ordered_index
) )
if layer.is_visible_in_hierarchy(): # Update the origin
var opacity := cel.get_final_opacity(layer)
layer_metadata_image.set_pixel(ordered_index, 1, Color(opacity, 0.0, 0.0, 0.0))
else:
layer_metadata_image.set_pixel(ordered_index, 1, Color())
var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size()) var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
layer_metadata_image.set_pixel( layer_metadata_image.set_pixel(
ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0) ordered_index, 2, Color(origin.x, origin.y, 0.0, 0.0)

View file

@ -76,9 +76,10 @@ func _draw_layers() -> void:
var current_frame := project.frames[frame_index] var current_frame := project.frames[frame_index]
var current_cels := current_frame.cels var current_cels := current_frame.cels
var textures: Array[Image] = [] var textures: Array[Image] = []
# Nx3 texture, where N is the number of layers and the first row are the blend modes, # Nx4 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 # the second are the opacities, the third are the origins and the fourth are the
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8) # clipping mask booleans.
var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8)
# Draw current frame layers # Draw current frame layers
for i in project.ordered_layers: for i in project.ordered_layers:
var cel := current_cels[i] var cel := current_cels[i]
@ -91,12 +92,7 @@ func _draw_layers() -> void:
else: else:
cel_image = cel.get_image() cel_image = cel.get_image()
textures.append(cel_image) textures.append(cel_image)
metadata_image.set_pixel(i, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i)
if layer.is_visible_in_hierarchy():
var opacity := cel.get_final_opacity(layer)
metadata_image.set_pixel(i, 1, Color(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() var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures) texture_array.create_from_images(textures)
material.set_shader_parameter("layers", texture_array) material.set_shader_parameter("layers", texture_array)

View file

@ -940,13 +940,11 @@ func _on_MergeDownLayer_pressed() -> void:
var top_image := top_layer.display_effects(top_cel) var top_image := top_layer.display_effects(top_cel)
var bottom_cel := frame.cels[bottom_layer.index] var bottom_cel := frame.cels[bottom_layer.index]
var textures: Array[Image] = [] var textures: Array[Image] = []
var metadata_image := Image.create(2, 3, false, Image.FORMAT_R8)
textures.append(bottom_cel.get_image()) textures.append(bottom_cel.get_image())
metadata_image.set_pixel(0, 1, Color(1.0, 0.0, 0.0, 0.0))
textures.append(top_image) textures.append(top_image)
metadata_image.set_pixel(1, 0, Color(top_layer.blend_mode / 255.0, 0.0, 0.0, 0.0)) var metadata_image := Image.create(2, 4, false, Image.FORMAT_R8)
var opacity := frame.cels[top_layer.index].get_final_opacity(top_layer) DrawingAlgos.set_layer_metadata_image(bottom_layer, bottom_cel, metadata_image, 0)
metadata_image.set_pixel(1, 1, Color(opacity, 0.0, 0.0, 0.0)) DrawingAlgos.set_layer_metadata_image(top_layer, top_cel, metadata_image, 1)
var texture_array := Texture2DArray.new() var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures) texture_array.create_from_images(textures)
var params := { var params := {

View file

@ -12,6 +12,8 @@ var layer_index := 0
@onready var line_edit := %LayerNameLineEdit as LineEdit @onready var line_edit := %LayerNameLineEdit as LineEdit
@onready var hierarchy_spacer := %HierarchySpacer as Control @onready var hierarchy_spacer := %HierarchySpacer as Control
@onready var linked_button := %LinkButton as BaseButton @onready var linked_button := %LinkButton as BaseButton
@onready var clipping_mask_icon := %ClippingMask as TextureRect
@onready var popup_menu := $PopupMenu as PopupMenu
func _ready() -> void: func _ready() -> void:
@ -71,6 +73,8 @@ func update_buttons() -> void:
visibility_button.modulate.a = 1 visibility_button.modulate.a = 1
lock_button.modulate.a = 1 lock_button.modulate.a = 1
popup_menu.set_item_checked(0, layer.clipping_mask)
clipping_mask_icon.visible = layer.clipping_mask
if is_instance_valid(layer.parent): if is_instance_valid(layer.parent):
if not layer.parent.is_visible_in_hierarchy(): if not layer.parent.is_visible_in_hierarchy():
visibility_button.modulate.a = 0.33 visibility_button.modulate.a = 0.33
@ -108,7 +112,9 @@ func _input(event: InputEvent) -> void:
func _on_LayerContainer_gui_input(event: InputEvent) -> void: func _on_LayerContainer_gui_input(event: InputEvent) -> void:
var project := Global.current_project var project := Global.current_project
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT: if not event is InputEventMouseButton:
return
if event.button_index == MOUSE_BUTTON_LEFT:
Global.canvas.selection.transform_content_confirm() Global.canvas.selection.transform_content_confirm()
var prev_curr_layer := project.current_layer var prev_curr_layer := project.current_layer
if Input.is_action_pressed(&"shift"): if Input.is_action_pressed(&"shift"):
@ -135,6 +141,10 @@ func _on_LayerContainer_gui_input(event: InputEvent) -> void:
line_edit.visible = true line_edit.visible = true
line_edit.editable = true line_edit.editable = true
line_edit.grab_focus() line_edit.grab_focus()
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
var layer := Global.current_project.layers[layer_index]
if not layer is GroupLayer:
popup_menu.popup(Rect2(get_global_mouse_position(), Vector2.ONE))
func _on_LineEdit_focus_exited() -> void: func _on_LineEdit_focus_exited() -> void:
@ -368,3 +378,12 @@ func _get_region_rect(y_begin: float, y_end: float) -> Rect2:
rect.position.y += rect.size.y * y_begin rect.position.y += rect.size.y * y_begin
rect.size.y *= y_end - y_begin rect.size.y *= y_end - y_begin
return rect return rect
func _on_popup_menu_id_pressed(id: int) -> void:
var layer := Global.current_project.layers[layer_index]
if id == 0:
layer.clipping_mask = not layer.clipping_mask
popup_menu.set_item_checked(0, layer.clipping_mask)
clipping_mask_icon.visible = layer.clipping_mask
Global.canvas.draw_layers()

View file

@ -1,10 +1,11 @@
[gd_scene load_steps=6 format=3 uid="uid://bai814sqvk68f"] [gd_scene load_steps=7 format=3 uid="uid://bai814sqvk68f"]
[ext_resource type="Script" path="res://src/UI/Timeline/LayerButton.gd" id="1_6hlpe"] [ext_resource type="Script" path="res://src/UI/Timeline/LayerButton.gd" id="1_6hlpe"]
[ext_resource type="Texture2D" uid="uid://c2b3htff5yox8" path="res://assets/graphics/layers/layer_visible.png" id="2_ef6fb"] [ext_resource type="Texture2D" uid="uid://c2b3htff5yox8" path="res://assets/graphics/layers/layer_visible.png" id="2_ef6fb"]
[ext_resource type="Texture2D" uid="uid://dndlglvqc7v6a" path="res://assets/graphics/layers/group_expanded.png" id="2_enrtd"] [ext_resource type="Texture2D" uid="uid://dndlglvqc7v6a" path="res://assets/graphics/layers/group_expanded.png" id="2_enrtd"]
[ext_resource type="Texture2D" uid="uid://dhc0pnnqojd2m" path="res://assets/graphics/layers/unlock.png" id="3_ah1my"] [ext_resource type="Texture2D" uid="uid://dhc0pnnqojd2m" path="res://assets/graphics/layers/unlock.png" id="3_ah1my"]
[ext_resource type="Texture2D" uid="uid://cofw1x6chh4i" path="res://assets/graphics/layers/unlinked_layer.png" id="4_058qm"] [ext_resource type="Texture2D" uid="uid://cofw1x6chh4i" path="res://assets/graphics/layers/unlinked_layer.png" id="4_058qm"]
[ext_resource type="Texture2D" uid="uid://ieo8fsapcgsy" path="res://assets/graphics/layers/clipping_mask.png" id="6_73j5q"]
[node name="LayerButton" type="Button"] [node name="LayerButton" type="Button"]
offset_right = 200.0 offset_right = 200.0
@ -147,6 +148,13 @@ unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
[node name="ClippingMask" type="TextureRect" parent="HBoxContainer/LayerName"]
unique_name_in_owner = true
visible = false
layout_mode = 2
texture = ExtResource("6_73j5q")
stretch_mode = 5
[node name="LayerNameLabel" type="Label" parent="HBoxContainer/LayerName"] [node name="LayerNameLabel" type="Label" parent="HBoxContainer/LayerName"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
@ -169,9 +177,17 @@ caret_blink_interval = 0.5
layout_mode = 2 layout_mode = 2
mouse_filter = 2 mouse_filter = 2
[node name="PopupMenu" type="PopupMenu" parent="."]
disable_3d = true
item_count = 1
item_0/text = "Clipping mask"
item_0/checkable = 1
item_0/id = 0
[connection signal="gui_input" from="." to="." method="_on_LayerContainer_gui_input"] [connection signal="gui_input" from="." to="." method="_on_LayerContainer_gui_input"]
[connection signal="pressed" from="HBoxContainer/LayerButtons/ExpandButton" to="." method="_on_ExpandButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/ExpandButton" to="." method="_on_ExpandButton_pressed"]
[connection signal="pressed" from="HBoxContainer/LayerButtons/VisibilityButton" to="." method="_on_VisibilityButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/VisibilityButton" to="." method="_on_VisibilityButton_pressed"]
[connection signal="pressed" from="HBoxContainer/LayerButtons/LockButton" to="." method="_on_LockButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/LockButton" to="." method="_on_LockButton_pressed"]
[connection signal="pressed" from="HBoxContainer/LayerButtons/LinkButton" to="." method="_on_LinkButton_pressed"] [connection signal="pressed" from="HBoxContainer/LayerButtons/LinkButton" to="." method="_on_LinkButton_pressed"]
[connection signal="focus_exited" from="HBoxContainer/LayerName/LayerNameLineEdit" to="." method="_on_LineEdit_focus_exited"] [connection signal="focus_exited" from="HBoxContainer/LayerName/LayerNameLineEdit" to="." method="_on_LineEdit_focus_exited"]
[connection signal="id_pressed" from="PopupMenu" to="." method="_on_popup_menu_id_pressed"]