added blend modes

This commit is contained in:
2025-07-20 22:14:42 +10:00
parent ef8ecb3732
commit 43dfc410dc
10 changed files with 242 additions and 6 deletions

View File

@@ -15,7 +15,15 @@ Added a proper opacity slider for sprites, with an "affect children" toggle. Cli
![Opacity Demo](https://github.com/user-attachments/assets/d5a05508-1cb1-46e0-8c90-d0aac73398db)
### 🌀 Rotation Wobble
### <EFBFBD> Blend Modes
Photoshop-style blend modes for sprites! Choose from Normal, Multiply, Screen, Overlay, Soft Light, Hard Light, Color Dodge, Color Burn, Darken, Lighten, Add, and Subtract. Perfect for lighting effects, shadows, or artistic styling.
* Dropdown selector in the sprite viewer menu
* Works alongside opacity controls
### <20>🌀 Rotation Wobble
Sprites can now do a subtle (or chaotic) rotational wobble. Frequency + amplitude sliders included.

View File

@@ -0,0 +1,58 @@
extends Node
## Blend Mode Manager
##
## Manages blend mode constants and provides utility functions for
## working with blend modes in the PNGTuber application.
## Blend mode constants
enum BlendMode {
NORMAL = 0,
MULTIPLY = 1,
SCREEN = 2,
OVERLAY = 3,
SOFT_LIGHT = 4,
HARD_LIGHT = 5,
COLOR_DODGE = 6,
COLOR_BURN = 7,
DARKEN = 8,
LIGHTEN = 9,
ADD = 10, # Linear Dodge
SUBTRACT = 11
}
## Returns an array of blend mode names for use in dropdowns
static func get_blend_mode_names() -> Array:
return [
"Normal",
"Multiply",
"Screen",
"Overlay",
"Soft Light",
"Hard Light",
"Color Dodge",
"Color Burn",
"Darken",
"Lighten",
"Add",
"Subtract"
]
## Returns the string name for a given blend mode enum value
static func get_blend_mode_name(mode: BlendMode) -> String:
var names = get_blend_mode_names()
if mode >= 0 and mode < names.size():
return names[mode]
return "Normal"
## Returns the blend mode enum value for a given name
static func get_blend_mode_from_name(name: String) -> BlendMode:
var names = get_blend_mode_names()
var index = names.find(name)
if index != -1:
return index as BlendMode
return BlendMode.NORMAL
## Returns whether a blend mode is valid
static func is_valid_blend_mode(mode: int) -> bool:
return mode >= BlendMode.NORMAL and mode <= BlendMode.SUBTRACT

View File

@@ -0,0 +1 @@
uid://bm1aeoarbpp6b

View File

@@ -527,6 +527,8 @@ func _on_load_dialog_file_selected(path):
sprite.spriteOpacity = data[item]["spriteOpacity"]
if data[item].has("affectChildrenOpacity"):
sprite.affectChildrenOpacity = data[item]["affectChildrenOpacity"]
if data[item].has("blendMode"):
sprite.blendMode = data[item]["blendMode"]
# Load wobble sync group (with backward compatibility)
if data[item].has("wobbleSyncGroup"):
@@ -638,6 +640,7 @@ func _on_save_dialog_file_selected(path):
data[id]["spriteOpacity"] = child.spriteOpacity
data[id]["affectChildrenOpacity"] = child.affectChildrenOpacity
data[id]["blendMode"] = child.blendMode
# Save wobble sync group
data[id]["wobbleSyncGroup"] = child.wobbleSyncGroup
@@ -724,6 +727,7 @@ func _on_duplicate_button_pressed():
sprite.spriteOpacity = Global.heldSprite.spriteOpacity
sprite.affectChildrenOpacity = Global.heldSprite.affectChildrenOpacity
sprite.blendMode = Global.heldSprite.blendMode
# Copy wobble sync group
sprite.wobbleSyncGroup = Global.heldSprite.wobbleSyncGroup

View File

@@ -30,6 +30,7 @@ buses/channel_disable_time=0.0
Saving="*res://autoload/saving.gd"
Global="*res://autoload/global.gd"
WobbleSyncManager="*res://autoload/wobble_sync_manager.gd"
BlendModeManager="*res://autoload/blend_mode_manager.gd"
ElgatoStreamDeck="*res://addons/godot-streamdeck-addon/singleton.gd"
DefaultAvatarData="*res://autoload/defaultAvatarData.gd"

View File

@@ -0,0 +1,122 @@
shader_type canvas_item;
uniform float opacity : hint_range(0.0, 1.0) = 1.0;
uniform int blend_mode : hint_range(0, 11) = 0;
// Blend mode constants:
// 0: Normal
// 1: Multiply
// 2: Screen
// 3: Overlay
// 4: Soft Light
// 5: Hard Light
// 6: Color Dodge
// 7: Color Burn
// 8: Darken
// 9: Lighten
// 10: Add (Linear Dodge)
// 11: Subtract
vec3 multiply_blend(vec3 base, vec3 overlay) {
return base * overlay;
}
vec3 screen_blend(vec3 base, vec3 overlay) {
return 1.0 - (1.0 - base) * (1.0 - overlay);
}
vec3 overlay_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = base.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
result.g = base.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
result.b = base.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
return result;
}
vec3 soft_light_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r + base.r * base.r * (1.0 - 2.0 * overlay.r) :
sqrt(base.r) * (2.0 * overlay.r - 1.0) + 2.0 * base.r * (1.0 - overlay.r);
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g + base.g * base.g * (1.0 - 2.0 * overlay.g) :
sqrt(base.g) * (2.0 * overlay.g - 1.0) + 2.0 * base.g * (1.0 - overlay.g);
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b + base.b * base.b * (1.0 - 2.0 * overlay.b) :
sqrt(base.b) * (2.0 * overlay.b - 1.0) + 2.0 * base.b * (1.0 - overlay.b);
return result;
}
vec3 hard_light_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r < 0.5 ? 2.0 * base.r * overlay.r : 1.0 - 2.0 * (1.0 - base.r) * (1.0 - overlay.r);
result.g = overlay.g < 0.5 ? 2.0 * base.g * overlay.g : 1.0 - 2.0 * (1.0 - base.g) * (1.0 - overlay.g);
result.b = overlay.b < 0.5 ? 2.0 * base.b * overlay.b : 1.0 - 2.0 * (1.0 - base.b) * (1.0 - overlay.b);
return result;
}
vec3 color_dodge_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r >= 1.0 ? 1.0 : min(1.0, base.r / (1.0 - overlay.r));
result.g = overlay.g >= 1.0 ? 1.0 : min(1.0, base.g / (1.0 - overlay.g));
result.b = overlay.b >= 1.0 ? 1.0 : min(1.0, base.b / (1.0 - overlay.b));
return result;
}
vec3 color_burn_blend(vec3 base, vec3 overlay) {
vec3 result;
result.r = overlay.r <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.r) / overlay.r);
result.g = overlay.g <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.g) / overlay.g);
result.b = overlay.b <= 0.0 ? 0.0 : max(0.0, 1.0 - (1.0 - base.b) / overlay.b);
return result;
}
vec3 darken_blend(vec3 base, vec3 overlay) {
return min(base, overlay);
}
vec3 lighten_blend(vec3 base, vec3 overlay) {
return max(base, overlay);
}
vec3 add_blend(vec3 base, vec3 overlay) {
return min(vec3(1.0), base + overlay);
}
vec3 subtract_blend(vec3 base, vec3 overlay) {
return max(vec3(0.0), base - overlay);
}
void fragment() {
vec4 tex_color = texture(TEXTURE, UV);
// Apply blend mode (only to RGB, preserve alpha channel behavior)
vec3 final_color;
if (blend_mode == 0) { // Normal
final_color = tex_color.rgb;
} else if (blend_mode == 1) { // Multiply
final_color = multiply_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 2) { // Screen
final_color = screen_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 3) { // Overlay
final_color = overlay_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 4) { // Soft Light
final_color = soft_light_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 5) { // Hard Light
final_color = hard_light_blend(vec3(0.5), tex_color.rgb);
} else if (blend_mode == 6) { // Color Dodge
final_color = color_dodge_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 7) { // Color Burn
final_color = color_burn_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 8) { // Darken
final_color = darken_blend(vec3(1.0), tex_color.rgb);
} else if (blend_mode == 9) { // Lighten
final_color = lighten_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 10) { // Add (Linear Dodge)
final_color = add_blend(vec3(0.0), tex_color.rgb);
} else if (blend_mode == 11) { // Subtract
final_color = subtract_blend(vec3(1.0), tex_color.rgb);
} else {
final_color = tex_color.rgb;
}
COLOR = vec4(final_color, tex_color.a * opacity);
}

View File

@@ -0,0 +1 @@
uid://dy03x5g364v80

View File

@@ -28,7 +28,7 @@ var imageSize = Vector2.ZERO
@onready var wob = $WobbleOrigin
@onready var outlineScene = preload("res://ui_scenes/selectedSprite/outline.tscn")
@onready var opacityShader = preload("res://ui_scenes/selectedSprite/opacity.gdshader")
@onready var opacityBlendShader = preload("res://ui_scenes/selectedSprite/opacity_blend.gdshader")
var opacityMaterial = null
@@ -90,6 +90,7 @@ var time = 0.0 # Time accumulator for physics calculations
#Opacity
var spriteOpacity = 1.0
var affectChildrenOpacity = false
var blendMode = 0 # Default to Normal blend mode
#Vis toggle
var toggle = "null"
@@ -517,14 +518,21 @@ func updateShaderOpacity():
var editorAlpha = sprite.modulate.a
var finalOpacity = totalOpacity * editorAlpha
# Always apply the opacity shader to ensure it's loaded
# Always apply the opacity blend shader to ensure it's loaded
if opacityMaterial == null:
opacityMaterial = ShaderMaterial.new()
opacityMaterial.shader = opacityShader
opacityMaterial.shader = opacityBlendShader
opacityMaterial.set_shader_parameter("opacity", finalOpacity)
opacityMaterial.set_shader_parameter("blend_mode", blendMode)
sprite.material = opacityMaterial
## Update the blend mode and refresh the shader
func updateBlendMode(newBlendMode: int):
if BlendModeManager.is_valid_blend_mode(newBlendMode):
blendMode = newBlendMode
updateShaderOpacity() # Refresh shader with new blend mode
func updateChildrenOpacityRecursively():
var linkedSprites = getAllLinkedSprites()
for child in linkedSprites:

View File

@@ -10,6 +10,7 @@ extends Node2D
@onready var coverCollider = $Area2D/CollisionShape2D
@onready var wobbleSyncControl = null # Will be assigned in _ready()
@onready var blendModeDropdown = $Opacity/blendModeDropdown
func _ready():
Global.spriteEdit = self
@@ -19,6 +20,9 @@ func _ready():
if wobbleSyncControl == null:
print("Warning: WobbleSyncControl node not found in sprite viewer")
# Set up blend mode dropdown
setupBlendModeDropdown()
func setImage():
if Global.heldSprite == null:
return
@@ -81,6 +85,7 @@ func setImage():
$Opacity/opacityLabel.text = "opacity: " + str(int(Global.heldSprite.spriteOpacity * 100)) + "%"
$Opacity/opacitySlider.value = Global.heldSprite.spriteOpacity
$Opacity/affectChildrenCheck.button_pressed = Global.heldSprite.affectChildrenOpacity
$Opacity/blendModeDropdown.selected = Global.heldSprite.blendMode
$VisToggle/setToggle/Label.text = "toggle: \"" + Global.heldSprite.toggle + "\""
@@ -449,3 +454,16 @@ func _on_opacity_slider_value_changed(value):
func _on_affect_children_check_toggled(button_pressed):
Global.heldSprite.affectChildrenOpacity = button_pressed
Global.heldSprite.updateOpacity()
## Set up the blend mode dropdown with all available blend modes
func setupBlendModeDropdown():
blendModeDropdown.clear()
var blendModeNames = BlendModeManager.get_blend_mode_names()
for mode_name in blendModeNames:
blendModeDropdown.add_item(mode_name)
## Handle blend mode dropdown selection
func _on_blend_mode_dropdown_item_selected(index: int):
if Global.heldSprite != null:
Global.heldSprite.updateBlendMode(index)
Global.pushUpdate("Blend mode set to: " + BlendModeManager.get_blend_mode_name(index))

View File

@@ -30,7 +30,7 @@
[ext_resource type="Texture2D" uid="uid://gflmeocxfnrq" path="res://ui_scenes/settings/deleteHotkey.png" id="27_fgu6g"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_21kva"]
size = Vector2(245, 1550)
size = Vector2(245, 1597)
[sub_resource type="ShaderMaterial" id="ShaderMaterial_hxmah"]
shader = ExtResource("3_ky0lu")
@@ -14286,7 +14286,7 @@ script = ExtResource("1_hp0e3")
z_index = -2
[node name="CollisionShape2D" type="CollisionShape2D" parent="Area2D"]
position = Vector2(122.5, 775)
position = Vector2(122.5, 798.5)
shape = SubResource("RectangleShape2D_21kva")
[node name="Border" type="Sprite2D" parent="."]
@@ -14768,6 +14768,20 @@ offset_right = 200.0
offset_bottom = 1166.0
theme = SubResource("Theme_mrbyn")
[node name="blendModeLabel" type="Label" parent="Opacity"]
offset_left = 11.0
offset_top = 2152.0
offset_right = 159.0
offset_bottom = 2178.0
text = "blend mode:"
[node name="blendModeDropdown" type="OptionButton" parent="Opacity"]
offset_left = 10.0
offset_top = 2173.0
offset_right = 233.0
offset_bottom = 2193.0
theme = SubResource("Theme_mrbyn")
[node name="Layers" type="Node2D" parent="."]
position = Vector2(0, 538)
@@ -14974,6 +14988,7 @@ centered = false
[connection signal="value_changed" from="Animation/animSpeed" to="." method="_on_anim_speed_value_changed"]
[connection signal="value_changed" from="Opacity/opacitySlider" to="." method="_on_opacity_slider_value_changed"]
[connection signal="toggled" from="Opacity/affectChildrenCheck" to="." method="_on_affect_children_check_toggled"]
[connection signal="item_selected" from="Opacity/blendModeDropdown" to="." method="_on_blend_mode_dropdown_item_selected"]
[connection signal="pressed" from="Layers/Layer1/layerButton1" to="." method="_on_layer_button_1_pressed"]
[connection signal="pressed" from="Layers/Layer2/layerButton2" to="." method="_on_layer_button_2_pressed"]
[connection signal="pressed" from="Layers/Layer3/layerButton3" to="." method="_on_layer_button_3_pressed"]