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

Implement a color curves image effect

Massive thanks to Material Maker for the custom widget code. The color curves effect is still WIP, I need to make the tangent points visible (not yet sure why they aren't now), add some curve presets, and implement it as a layer effect as well.
This commit is contained in:
Emmanouil Papadeas 2024-12-15 18:10:55 +02:00
parent 605bff7324
commit 048058bd35
9 changed files with 503 additions and 0 deletions

View file

@ -685,6 +685,10 @@ adjust_brightness_contrast={
"deadzone": 0.5, "deadzone": 0.5,
"events": [] "events": []
} }
color_curves={
"deadzone": 0.5,
"events": []
}
gradient={ gradient={
"deadzone": 0.5, "deadzone": 0.5,
"events": [] "events": []

View file

@ -64,6 +64,7 @@ enum EffectsMenu {
DESATURATION, DESATURATION,
HSV, HSV,
BRIGHTNESS_SATURATION, BRIGHTNESS_SATURATION,
COLOR_CURVES,
PALETTIZE, PALETTIZE,
PIXELIZE, PIXELIZE,
POSTERIZE, POSTERIZE,
@ -809,6 +810,7 @@ func _initialize_keychain() -> void:
&"drop_shadow": Keychain.InputAction.new("", "Effects menu", true), &"drop_shadow": Keychain.InputAction.new("", "Effects menu", true),
&"adjust_hsv": Keychain.InputAction.new("", "Effects menu", true), &"adjust_hsv": Keychain.InputAction.new("", "Effects menu", true),
&"adjust_brightness_contrast": Keychain.InputAction.new("", "Effects menu", true), &"adjust_brightness_contrast": Keychain.InputAction.new("", "Effects menu", true),
&"color_curves": Keychain.InputAction.new("", "Effects menu", true),
&"gaussian_blur": Keychain.InputAction.new("", "Effects menu", true), &"gaussian_blur": Keychain.InputAction.new("", "Effects menu", true),
&"gradient": Keychain.InputAction.new("", "Effects menu", true), &"gradient": Keychain.InputAction.new("", "Effects menu", true),
&"gradient_map": Keychain.InputAction.new("", "Effects menu", true), &"gradient_map": Keychain.InputAction.new("", "Effects menu", true),

View file

@ -0,0 +1,67 @@
shader_type canvas_item;
// CurveTexture(s)
uniform sampler2D curve_rgb;
uniform sampler2D curve_red;
uniform sampler2D curve_green;
uniform sampler2D curve_blue;
uniform sampler2D curve_alpha;
uniform sampler2D curve_hue;
uniform sampler2D curve_sat;
uniform sampler2D curve_value;
uniform sampler2D selection : filter_nearest;
vec3 rgb2hsb(vec3 c) {
vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
vec4 p = mix(vec4(c.bg, K.wz),
vec4(c.gb, K.xy),
step(c.b, c.g));
vec4 q = mix(vec4(p.xyw, c.r),
vec4(c.r, p.yzx),
step(p.x, c.r));
float d = q.x - min(q.w, q.y);
float e = 1.0e-10;
return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
d / (q.x + e),
q.x);
}
vec3 hsb2rgb(vec3 c)
{
vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}
void fragment() {
vec4 original_color = texture(TEXTURE, UV);
vec4 selection_color = texture(selection, UV);
vec4 col;
float red_curve_color = texture(curve_red, vec2(COLOR.r, 0.0)).r;
float green_curve_color = texture(curve_green, vec2(COLOR.g, 0.0)).r;
float blue_curve_color = texture(curve_blue, vec2(COLOR.b, 0.0)).r;
float alpha_curve_color = texture(curve_alpha, vec2(COLOR.a, 0.0)).r;
col.r = red_curve_color;
col.g = green_curve_color;
col.b = blue_curve_color;
col.a = alpha_curve_color;
vec3 hsb = rgb2hsb(col.rgb);
float hue_curve_color = texture(curve_hue, vec2(hsb.r, 0.0)).r;
float sat_curve_color = texture(curve_sat, vec2(hsb.g, 0.0)).r;
float value_curve_color = texture(curve_value, vec2(hsb.b, 0.0)).r;
hsb.r = hue_curve_color;
hsb.g = sat_curve_color;
hsb.b = value_curve_color;
col.rgb = hsb2rgb(hsb);
float rgb_curve_color_r = texture(curve_rgb, vec2(col.r, 0.0)).r;
float rgb_curve_color_g = texture(curve_rgb, vec2(col.g, 0.0)).r;
float rgb_curve_color_b = texture(curve_rgb, vec2(col.b, 0.0)).r;
col.rgb = vec3(rgb_curve_color_r, rgb_curve_color_g, rgb_curve_color_b);
vec4 output = mix(original_color.rgba, col, selection_color.a);
COLOR = output;
}

View file

@ -0,0 +1,50 @@
extends ImageEffect
enum Channel { RGB, RED, GREEN, BLUE, ALPHA, HUE, SATURATION, VALUE }
const SHADER := preload("res://src/Shaders/Effects/ColorCurves.gdshader")
var curves: Array[Curve]
@onready var channel_option_button := %ChannelOptionButton as OptionButton
@onready var curve_edit := $VBoxContainer/CurveEdit as CurveEdit
func _ready() -> void:
super._ready()
var sm := ShaderMaterial.new()
sm.shader = SHADER
preview.set_material(sm)
for i in channel_option_button.item_count:
var curve := Curve.new()
curve.add_point(Vector2.ZERO, 0, 1, Curve.TANGENT_LINEAR)
curve.add_point(Vector2.ONE, 1, 0, Curve.TANGENT_LINEAR)
curves.append(curve)
curve_edit.curve = curves[Channel.RGB]
func commit_action(cel: Image, project := Global.current_project) -> void:
var selection_tex: ImageTexture
if selection_checkbox.button_pressed and project.has_selection:
var selection := project.selection_map.return_cropped_copy(project.size)
selection_tex = ImageTexture.create_from_image(selection)
var params := {
"curve_rgb": CurveEdit.to_texture(curves[Channel.RGB]),
"curve_red": CurveEdit.to_texture(curves[Channel.RED]),
"curve_green": CurveEdit.to_texture(curves[Channel.GREEN]),
"curve_blue": CurveEdit.to_texture(curves[Channel.BLUE]),
"curve_alpha": CurveEdit.to_texture(curves[Channel.ALPHA]),
"curve_hue": CurveEdit.to_texture(curves[Channel.HUE]),
"curve_sat": CurveEdit.to_texture(curves[Channel.SATURATION]),
"curve_value": CurveEdit.to_texture(curves[Channel.VALUE]),
"selection": selection_tex
}
if !has_been_confirmed:
for param in params:
preview.material.set_shader_parameter(param, params[param])
else:
var gen := ShaderImageEffect.new()
gen.generate_image(cel, SHADER, params, project.size)
func _on_channel_option_button_item_selected(index: int) -> void:
curve_edit.curve = curves[index]

View file

@ -0,0 +1,60 @@
[gd_scene load_steps=5 format=3 uid="uid://cthknpr74lawl"]
[ext_resource type="PackedScene" uid="uid://bybqhhayl5ay5" path="res://src/UI/Dialogs/ImageEffects/ImageEffectParent.tscn" id="1_4g7xo"]
[ext_resource type="Script" path="res://src/UI/Dialogs/ImageEffects/ColorCurvesDialog.gd" id="2_xkivc"]
[ext_resource type="Script" path="res://src/UI/Nodes/CurveEditor/CurveEdit.gd" id="3_fp0lm"]
[sub_resource type="Curve" id="Curve_p6hdh"]
_data = [Vector2(0, 0), 0.0, 1.0, 0, 1, Vector2(1, 1), 1.0, 0.0, 1, 0]
point_count = 2
[node name="ColorCurvesDialog" instance=ExtResource("1_4g7xo")]
title = "Color Curves"
size = Vector2i(362, 440)
script = ExtResource("2_xkivc")
[node name="VBoxContainer" parent="." index="3"]
offset_bottom = 391.0
[node name="ShowAnimate" parent="VBoxContainer" index="0"]
visible = false
[node name="ColorOptions" type="GridContainer" parent="VBoxContainer" index="3"]
layout_mode = 2
columns = 2
[node name="ChannelLabel" type="Label" parent="VBoxContainer/ColorOptions" index="0"]
layout_mode = 2
size_flags_horizontal = 3
text = "Channel:"
[node name="ChannelOptionButton" type="OptionButton" parent="VBoxContainer/ColorOptions" index="1"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
mouse_default_cursor_shape = 2
selected = 0
item_count = 8
popup/item_0/text = "RGB"
popup/item_1/text = "Red"
popup/item_1/id = 1
popup/item_2/text = "Green"
popup/item_2/id = 2
popup/item_3/text = "Blue"
popup/item_3/id = 3
popup/item_4/text = "Alpha"
popup/item_4/id = 4
popup/item_5/text = "Hue"
popup/item_5/id = 5
popup/item_6/text = "Saturation"
popup/item_6/id = 6
popup/item_7/text = "Value"
popup/item_7/id = 7
[node name="CurveEdit" type="Control" parent="VBoxContainer" index="4"]
layout_mode = 2
size_flags_vertical = 3
script = ExtResource("3_fp0lm")
curve = SubResource("Curve_p6hdh")
[connection signal="item_selected" from="VBoxContainer/ColorOptions/ChannelOptionButton" to="." method="_on_channel_option_button_item_selected"]

View file

@ -0,0 +1,104 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/control_point.gd
class_name CurveEditControlPoint
extends Control
signal moved(index: int)
signal removed(index: int)
const OFFSET := Vector2(3, 3)
var moving := false
var min_x: float
var max_x: float
var min_y: float
var max_y: float
var left_slope: CurveEditTangentPoint
var right_slope: CurveEditTangentPoint
@onready var parent := get_parent() as CurveEdit
func _ready() -> void:
custom_minimum_size = Vector2(8, 8)
left_slope = CurveEditTangentPoint.new()
right_slope = CurveEditTangentPoint.new()
gui_input.connect(_on_gui_input)
left_slope.gui_input.connect(_on_gui_input)
right_slope.gui_input.connect(_on_gui_input)
add_child(left_slope)
add_child(right_slope)
func _draw() -> void:
var color := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
color = current_theme.get_color("font_color", "Label")
for c: CurveEditTangentPoint in get_children():
if c.visible:
draw_line(OFFSET, c.position + OFFSET, color)
draw_rect(Rect2(0, 0, 7, 7), color)
func initialize(curve: Curve, index: int) -> void:
if not is_instance_valid(parent):
await ready
position = parent.transform_point(curve.get_point_position(index)) - OFFSET
var left_tangent := curve.get_point_left_tangent(index)
var right_tangent := curve.get_point_right_tangent(index)
if left_tangent != INF:
left_slope.position = (
left_slope.distance * (parent.size * Vector2(1.0, -left_tangent)).normalized()
)
if right_tangent != INF:
right_slope.position = (
right_slope.distance * (parent.size * Vector2(1.0, -right_tangent)).normalized()
)
func set_control_point_visibility(left: bool, new_visible: bool) -> void:
if not is_instance_valid(left_slope):
await ready
if left:
left_slope.visible = new_visible
else:
right_slope.visible = new_visible
func set_constraint(new_min_x: float, new_max_x: float, new_min_y: float, new_max_y: float) -> void:
min_x = new_min_x
max_x = new_max_x
min_y = new_min_y
max_y = new_max_y
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
moving = true
else:
moving = false
parent.update_controls()
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
removed.emit(get_index())
elif moving and event is InputEventMouseMotion:
position += event.relative
if position.x < min_x:
position.x = min_x
elif position.x > max_x:
position.x = max_x
if position.y < min_y:
position.y = min_y
elif position.y > max_y:
position.y = max_y
moved.emit(get_index())
func update_tangents() -> void:
queue_redraw()
moved.emit(get_index())

View file

@ -0,0 +1,153 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/curve_view.gd
# and
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/curve_editor.gd
@tool
class_name CurveEdit
extends Control
signal value_changed(value: Curve)
@export var show_axes := true
@export var curve: Curve:
set(value):
curve = value
queue_redraw()
update_controls()
func _ready() -> void:
if not is_instance_valid(curve):
curve = Curve.new()
gui_input.connect(_on_gui_input)
resized.connect(_on_resize)
queue_redraw()
update_controls()
func update_controls() -> void:
for c in get_children():
c.queue_free()
for i in curve.point_count:
var p := curve.get_point_position(i)
var control_point := CurveEditControlPoint.new()
add_child(control_point)
control_point.initialize(curve, i)
control_point.position = transform_point(p) - control_point.OFFSET
if i == 0 or i == curve.point_count - 1:
control_point.set_constraint(
control_point.position.x,
control_point.position.x,
-control_point.OFFSET.y,
size.y - control_point.OFFSET.y
)
if i == 0:
control_point.set_control_point_visibility(true, false)
else:
control_point.set_control_point_visibility(false, false)
else:
var min_x := transform_point(curve.get_point_position(i - 1)).x + 1
var max_x := transform_point(curve.get_point_position(i + 1)).x - 1
control_point.set_constraint(
min_x, max_x, -control_point.OFFSET.y, size.y - control_point.OFFSET.y
)
control_point.moved.connect(_on_control_point_moved)
control_point.removed.connect(_on_control_point_removed)
value_changed.emit(curve)
static func to_texture(from_curve: Curve, width := 256) -> CurveTexture:
var texture := CurveTexture.new()
texture.texture_mode = CurveTexture.TEXTURE_MODE_RED
texture.curve = from_curve
texture.width = width
return texture
func transform_point(p: Vector2) -> Vector2:
return (Vector2(0.0, 1.0) + Vector2(1.0, -1.0) * p) * size
func reverse_transform_point(p: Vector2) -> Vector2:
return Vector2(0.0, 1.0) + Vector2(1.0, -1.0) * p / size
func _draw() -> void:
var bg := Color.DARK_GRAY
var fg := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
var panel_stylebox := current_theme.get_stylebox("panel", "Panel")
if panel_stylebox is StyleBoxFlat:
bg = panel_stylebox.bg_color
fg = current_theme.get_color("font_color", "Label")
var axes_color := bg.lerp(fg, 0.25)
var curve_color := bg.lerp(fg, 0.75)
if show_axes:
for i in range(5):
var p := transform_point(0.25 * Vector2(i, i))
draw_line(Vector2(p.x, 0), Vector2(p.x, size.y - 1), axes_color)
draw_line(Vector2(0, p.y), Vector2(size.x - 1, p.y), axes_color)
var points := PackedVector2Array()
for i in range(curve.point_count - 1):
var p1 := curve.get_point_position(i)
var p2 := curve.get_point_position(i + 1)
var d := (p2.x - p1.x) / 3.0
var yac := p1.y + d * curve.get_point_right_tangent(i)
var ybc := p2.y - d * curve.get_point_left_tangent(i + 1)
var p := transform_point(p1)
if points.is_empty():
points.push_back(p)
var count := maxi(1, transform_point(p2).x - p.x / 5.0)
for tt in range(count):
var t := (tt + 1.0) / count
var omt := 1.0 - t
var omt2 := omt * omt
var omt3 := omt2 * omt
var t2 := t * t
var t3 := t2 * t
var x := p1.x + (p2.x - p1.x) * t
var y := p1.y * omt3 + yac * omt2 * t * 3.0 + ybc * omt * t2 * 3.0 + p2.y * t3
p = transform_point(Vector2(x, y))
points.push_back(p)
draw_polyline(points, curve_color)
func _on_control_point_moved(index: int) -> void:
var control_point := get_child(index) as CurveEditControlPoint
var new_point := reverse_transform_point(control_point.position + control_point.OFFSET)
curve.set_point_offset(index, new_point.x)
curve.set_point_value(index, new_point.y)
if is_instance_valid(control_point.left_slope):
var slope_vector := control_point.left_slope.position / size
if slope_vector.x != 0:
curve.set_point_left_tangent(index, -slope_vector.y / slope_vector.x)
if is_instance_valid(control_point.right_slope):
var slope_vector := control_point.right_slope.position / size
if slope_vector.x != 0:
curve.set_point_right_tangent(index, -slope_vector.y / slope_vector.x)
queue_redraw()
value_changed.emit(curve)
func _on_control_point_removed(index: int) -> void:
if index > 0 and index < curve.point_count:
curve.remove_point(index)
queue_redraw()
update_controls()
func _on_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
var new_point_position := reverse_transform_point(get_local_mouse_position())
curve.add_point(new_point_position, 0.0, 0.0)
update_controls()
func _on_resize() -> void:
queue_redraw()
update_controls()

View file

@ -0,0 +1,59 @@
# Code taken and modified from Material Maker, licensed under MIT
# gdlint: ignore=max-line-length
# https://github.com/RodZill4/material-maker/blob/master/material_maker/widgets/curve_edit/slope_point.gd
class_name CurveEditTangentPoint
extends Control
const OFFSET := -Vector2(0, 0)
@export var distance: float
var moving = false
@onready var parent := get_parent() as CurveEditControlPoint
@onready var grandparent := parent.get_parent() as Control
func _ready() -> void:
custom_minimum_size = Vector2(8, 8)
func _draw() -> void:
var color := Color.GRAY
var current_scene := get_tree().current_scene
if current_scene is Control:
var current_theme := (current_scene as Control).theme
color = current_theme.get_color("font_color", "Label")
draw_circle(Vector2(3.0, 3.0), 3.0, color)
func _on_ControlPoint_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
if event.double_click:
var vector: Vector2
if get_index() == 0:
vector = (
parent.position - grandparent.get_child(parent.get_index() - 1).position
)
else:
vector = (
grandparent.get_child(parent.get_index() + 1).position - parent.position
)
vector = distance * vector.normalized()
position = vector - OFFSET
if event.is_control_or_command_pressed():
parent.get_child(1 - get_index()).position = -vector - OFFSET
parent.update_tangents()
else:
moving = true
else:
moving = false
elif moving and event is InputEventMouseMotion:
var vector := get_global_mouse_position() - parent.get_global_rect().position + OFFSET
vector *= signf(vector.x)
vector = distance * vector.normalized()
position = vector - OFFSET
if event.is_command_or_control_pressed():
parent.get_child(1 - get_index()).position = -vector - OFFSET
parent.update_tangents()

View file

@ -33,6 +33,7 @@ var hsv_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/HSVDialog.tscn")
var adjust_brightness_saturation_dialog := Dialog.new( var adjust_brightness_saturation_dialog := Dialog.new(
"res://src/UI/Dialogs/ImageEffects/BrightnessContrastDialog.tscn" "res://src/UI/Dialogs/ImageEffects/BrightnessContrastDialog.tscn"
) )
var color_curves_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/ColorCurvesDialog.tscn")
var gaussian_blur_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GaussianBlur.tscn") var gaussian_blur_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GaussianBlur.tscn")
var gradient_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientDialog.tscn") var gradient_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientDialog.tscn")
var gradient_map_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn") var gradient_map_dialog := Dialog.new("res://src/UI/Dialogs/ImageEffects/GradientMapDialog.tscn")
@ -449,6 +450,7 @@ func _setup_effects_menu() -> void:
"Desaturation": "desaturation", "Desaturation": "desaturation",
"Adjust Hue/Saturation/Value": "adjust_hsv", "Adjust Hue/Saturation/Value": "adjust_hsv",
"Adjust Brightness/Contrast": "adjust_brightness_contrast", "Adjust Brightness/Contrast": "adjust_brightness_contrast",
"Color Curves": "color_curves",
"Palettize": "palettize", "Palettize": "palettize",
"Pixelize": "pixelize", "Pixelize": "pixelize",
"Posterize": "posterize", "Posterize": "posterize",
@ -934,6 +936,8 @@ func effects_menu_id_pressed(id: int) -> void:
hsv_dialog.popup() hsv_dialog.popup()
Global.EffectsMenu.BRIGHTNESS_SATURATION: Global.EffectsMenu.BRIGHTNESS_SATURATION:
adjust_brightness_saturation_dialog.popup() adjust_brightness_saturation_dialog.popup()
Global.EffectsMenu.COLOR_CURVES:
color_curves_dialog.popup()
Global.EffectsMenu.GAUSSIAN_BLUR: Global.EffectsMenu.GAUSSIAN_BLUR:
gaussian_blur_dialog.popup() gaussian_blur_dialog.popup()
Global.EffectsMenu.GRADIENT: Global.EffectsMenu.GRADIENT: