mirror of
https://github.com/litruv/PNGTuber-Plus.git
synced 2026-07-24 02:26:02 +10:00
added blend modes
This commit is contained in:
10
README.md
10
README.md
@@ -15,7 +15,15 @@ Added a proper opacity slider for sprites, with an "affect children" toggle. Cli
|
||||

|
||||
|
||||
|
||||
### 🌀 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.
|
||||
|
||||
|
||||
58
autoload/blend_mode_manager.gd
Normal file
58
autoload/blend_mode_manager.gd
Normal 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
|
||||
1
autoload/blend_mode_manager.gd.uid
Normal file
1
autoload/blend_mode_manager.gd.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bm1aeoarbpp6b
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
122
ui_scenes/selectedSprite/opacity_blend.gdshader
Normal file
122
ui_scenes/selectedSprite/opacity_blend.gdshader
Normal 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);
|
||||
}
|
||||
1
ui_scenes/selectedSprite/opacity_blend.gdshader.uid
Normal file
1
ui_scenes/selectedSprite/opacity_blend.gdshader.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dy03x5g364v80
|
||||
@@ -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:
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user