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,
) -> void:
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
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
# Nx4 texture, where N is the number of layers and the first row are the blend modes,
# the second are the opacities, the third are the origins and the fourth are the
# clipping mask booleans.
var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8)
var frame_index := project.frames.find(frame)
var previous_ordered_layers: Array[int] = project.ordered_layers
project.order_layers(frame_index)
@ -51,14 +52,7 @@ func blend_layers(
var cel := frame.cels[ordered_index]
var cel_image := layer.display_effects(cel)
textures.append(cel_image)
# Store the blend mode
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())
set_layer_metadata_image(layer, cel, metadata_image, ordered_index, include)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {
@ -73,6 +67,24 @@ func blend_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
func get_ellipse_points(pos: Vector2i, size: Vector2i) -> 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 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 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 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.
@ -215,6 +216,7 @@ func serialize() -> Dictionary:
"visible": visible,
"locked": locked,
"blend_mode": blend_mode,
"clipping_mask": clipping_mask,
"opacity": opacity,
"parent": parent.index if is_instance_valid(parent) else -1,
"effects": effect_data
@ -233,13 +235,12 @@ func serialize() -> Dictionary:
## Sets the layer data according to a curated [Dictionary] obtained from [method serialize].
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.has("opacity"):
opacity = dict.opacity
name = dict.get("name", "")
visible = dict.get("visible", true)
locked = dict.get("locked", false)
blend_mode = dict.get("blend_mode", BlendModes.NORMAL)
clipping_mask = dict.get("clipping_mask", false)
opacity = dict.get("opacity", 1.0)
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

View file

@ -5,8 +5,9 @@ const float HCV_EPSILON = 1e-10;
const float HSL_EPSILON = 1e-10;
uniform sampler2DArray layers : filter_nearest;
// 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
// Nx4 texture, where N is the number of layers and the first row are the blend modes,
// the second are the opacities, the third are the origins and the fourth are the
// clipping mask booleans.
uniform sampler2D metadata : filter_nearest;
uniform bool origin_x_positive = true;
uniform bool origin_y_positive = true;
@ -153,9 +154,9 @@ void fragment() {
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;
vec4 result_color = texture(layers, vec3(UV - first_origin, 0.0));
result_color.a = border_trim(result_color, UV - first_origin);
result_color.a *= first_opacity;
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;
@ -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;
vec2 uv = UV - current_origin;
vec4 texture_color = texture(layers, vec3(uv, float(i)));
texture_color.a = border_trim(texture_color, uv);
col = blend(current_blend_mode, texture_color, col, current_opacity);
vec4 layer_color = texture(layers, vec3(uv, float(i)));
vec4 prev_layer_color = texture(layers, vec3(uv, float(i - 1)));
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:
var textures: Array[Image] = []
textures.resize(project.layers.size())
# 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)
# Nx4 texture, where N is the number of layers and the first row are the blend modes,
# the second are the opacities, the third are the origins and the fourth are the
# clipping mask booleans.
layer_metadata_image = Image.create(project.layers.size(), 4, false, Image.FORMAT_RG8)
# Draw current frame layers
for i in project.layers.size():
var ordered_index := project.ordered_layers[i]
@ -162,16 +163,7 @@ func draw_layers() -> void:
else:
cel_image = cel.get_image()
textures[ordered_index] = cel_image
# Store the blend mode
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())
DrawingAlgos.set_layer_metadata_image(layer, cel, layer_metadata_image, ordered_index)
# Store the origin
if [project.current_frame, i] in project.selected_cels:
var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
@ -199,14 +191,10 @@ func draw_layers() -> void:
else:
cel_image = cel.get_image()
layer_texture_array.update_layer(cel_image, ordered_index)
layer_metadata_image.set_pixel(
ordered_index, 0, Color(layer.blend_mode / 255.0, 0.0, 0.0, 0.0)
DrawingAlgos.set_layer_metadata_image(
layer, cel, layer_metadata_image, ordered_index
)
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())
# Update the origin
var origin := Vector2(move_preview_location).abs() / Vector2(cel_image.get_size())
layer_metadata_image.set_pixel(
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_cels := current_frame.cels
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
var metadata_image := Image.create(project.layers.size(), 3, false, Image.FORMAT_R8)
# Nx4 texture, where N is the number of layers and the first row are the blend modes,
# the second are the opacities, the third are the origins and the fourth are the
# clipping mask booleans.
var metadata_image := Image.create(project.layers.size(), 4, false, Image.FORMAT_R8)
# Draw current frame layers
for i in project.ordered_layers:
var cel := current_cels[i]
@ -91,12 +92,7 @@ func _draw_layers() -> void:
else:
cel_image = cel.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 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))
DrawingAlgos.set_layer_metadata_image(layer, cel, metadata_image, i)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
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 bottom_cel := frame.cels[bottom_layer.index]
var textures: Array[Image] = []
var metadata_image := Image.create(2, 3, false, Image.FORMAT_R8)
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)
metadata_image.set_pixel(1, 0, Color(top_layer.blend_mode / 255.0, 0.0, 0.0, 0.0))
var opacity := frame.cels[top_layer.index].get_final_opacity(top_layer)
metadata_image.set_pixel(1, 1, Color(opacity, 0.0, 0.0, 0.0))
var metadata_image := Image.create(2, 4, false, Image.FORMAT_R8)
DrawingAlgos.set_layer_metadata_image(bottom_layer, bottom_cel, metadata_image, 0)
DrawingAlgos.set_layer_metadata_image(top_layer, top_cel, metadata_image, 1)
var texture_array := Texture2DArray.new()
texture_array.create_from_images(textures)
var params := {

View file

@ -12,6 +12,8 @@ var layer_index := 0
@onready var line_edit := %LayerNameLineEdit as LineEdit
@onready var hierarchy_spacer := %HierarchySpacer as Control
@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:
@ -71,6 +73,8 @@ func update_buttons() -> void:
visibility_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 not layer.parent.is_visible_in_hierarchy():
visibility_button.modulate.a = 0.33
@ -108,7 +112,9 @@ func _input(event: InputEvent) -> void:
func _on_LayerContainer_gui_input(event: InputEvent) -> void:
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()
var prev_curr_layer := project.current_layer
if Input.is_action_pressed(&"shift"):
@ -135,6 +141,10 @@ func _on_LayerContainer_gui_input(event: InputEvent) -> void:
line_edit.visible = true
line_edit.editable = true
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:
@ -368,3 +378,12 @@ func _get_region_rect(y_begin: float, y_end: float) -> Rect2:
rect.position.y += rect.size.y * y_begin
rect.size.y *= y_end - y_begin
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="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://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://ieo8fsapcgsy" path="res://assets/graphics/layers/clipping_mask.png" id="6_73j5q"]
[node name="LayerButton" type="Button"]
offset_right = 200.0
@ -147,6 +148,13 @@ unique_name_in_owner = true
layout_mode = 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"]
unique_name_in_owner = true
layout_mode = 2
@ -169,9 +177,17 @@ caret_blink_interval = 0.5
layout_mode = 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="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/LockButton" to="." method="_on_LockButton_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="id_pressed" from="PopupMenu" to="." method="_on_popup_menu_id_pressed"]