diff --git a/.gitignore b/.gitignore index a2324d2..9835df9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ .export/ .itchio/ .steam/ -build/ \ No newline at end of file +build/ +*.TMP diff --git a/NDI_SETUP_GUIDE.md b/NDI_SETUP_GUIDE.md index e92f2e4..f7443cc 100644 --- a/NDI_SETUP_GUIDE.md +++ b/NDI_SETUP_GUIDE.md @@ -40,9 +40,18 @@ Access the NDI settings through the Settings Menu: - **NDI streaming**: Enable/disable NDI output - **Source name**: Set a custom name for your NDI source (default: "PNGTuber-Plus") - **Include audio**: Toggle audio streaming on/off + - **Avatar-only stream**: Enable a clean avatar-only stream without UI elements - **Status**: View current streaming status - **Start/Stop NDI**: Manual control button +#### Avatar-Only Streaming + +The "Avatar-only stream" option creates a second NDI source called "PNGTuber Avatar" that: +- Streams only the avatar sprites with a transparent background +- Excludes all UI elements, menus, and controls +- Perfect for professional streaming setups +- Syncs automatically with the main avatar state + ### 3. Hotkey Controls Quick keyboard shortcuts for NDI control: diff --git a/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll b/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll deleted file mode 100644 index 60e5fe6..0000000 Binary files a/addons/godot-git-plugin/win64/~libgit_plugin.windows.editor.x86_64.dll and /dev/null differ diff --git a/addons/godot-ndi/bin/windows/~libgodot-ndi.windows.template_debug.x86_64.dll b/addons/godot-ndi/bin/windows/~libgodot-ndi.windows.template_debug.x86_64.dll deleted file mode 100644 index 8212dde..0000000 Binary files a/addons/godot-ndi/bin/windows/~libgodot-ndi.windows.template_debug.x86_64.dll and /dev/null differ diff --git a/bin/~libgdexample.windows.template_debug.x86_64.dll b/bin/~libgdexample.windows.template_debug.x86_64.dll deleted file mode 100644 index c28b9ad..0000000 Binary files a/bin/~libgdexample.windows.template_debug.x86_64.dll and /dev/null differ diff --git a/main_scenes/ControlPanel.gd b/main_scenes/ControlPanel.gd index 9824948..5813306 100644 --- a/main_scenes/ControlPanel.gd +++ b/main_scenes/ControlPanel.gd @@ -4,7 +4,7 @@ extends Node2D @onready var editButton = $Edit/Button func _process(delta): - var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0 + var uiAnimSpeed = 8.0 # Fixed UI animation speed if Rect2(editButton.get_parent().position-Vector2(24,24),editButton.size).has_point(get_local_mouse_position()): editSprite.scale = lerp(editSprite.scale, Vector2(1.2,1.2), delta * uiAnimSpeed) else: diff --git a/main_scenes/EditControls.gd b/main_scenes/EditControls.gd index 3ef800b..61a1b63 100644 --- a/main_scenes/EditControls.gd +++ b/main_scenes/EditControls.gd @@ -30,7 +30,7 @@ func _process(delta): if buttons[b] == null: continue if Rect2(buttons[b].get_parent().position-Vector2(24,24),buttons[b].size).has_point(get_local_mouse_position()): - var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0 + var uiAnimSpeed = 8.0 # Fixed UI animation speed sprites[s].scale = lerp(sprites[s].scale, Vector2(1.2,1.2), delta * uiAnimSpeed) if Input.is_action_pressed("mouse_left"): sprites[s].scale = Vector2(0.6,0.6) @@ -51,7 +51,7 @@ func _process(delta): Global.mouse.text = "Duplicate sprite" else: - var uiAnimSpeed = Global.main.physicsTimeMultiplier * 20.0 if Global.main else 120.0 + var uiAnimSpeed = 8.0 # Fixed UI animation speed sprites[s].scale = lerp(sprites[s].scale, Vector2(1.0,1.0), delta * uiAnimSpeed) s += 1 diff --git a/main_scenes/NDIAvatarViewport.gd b/main_scenes/NDIAvatarViewport.gd new file mode 100644 index 0000000..6f907ad --- /dev/null +++ b/main_scenes/NDIAvatarViewport.gd @@ -0,0 +1,144 @@ +extends SubViewport + +## NDI Avatar Viewport Controller +## Handles avatar-only NDI streaming with synchronized states + +#Node Reference +@onready var origin = $OriginMotion/Origin +@onready var ndi_output = $NDIOutput + +## Initialize viewport settings for transparent background and avatar mirroring +func _ready(): + # Set transparent background + transparent_bg = true + + # Connect to the main scene for state sync + if Global.main: + Global.main.connect("spriteVisToggles", Callable(self, "_on_sprite_visibility_toggle")) + Global.main.connect("bounceChange", Callable(self, "_sync_bounce")) + +## Sync avatar sprites from main scene to this viewport +func sync_avatar(): + # Clear existing sprites + for child in origin.get_children(): + child.queue_free() + + # Wait for cleanup + await get_tree().process_frame + + # Get sprites from main scene + var main_sprites = get_tree().get_nodes_in_group("saved") + for sprite in main_sprites: + clone_sprite(sprite) + +## Clone a sprite from main scene to this viewport +func clone_sprite(original_sprite): + # Create new instance + var sprite_scene = preload("res://ui_scenes/selectedSprite/spriteObject.tscn") + var cloned_sprite = sprite_scene.instantiate() + + # Copy all properties + cloned_sprite.path = original_sprite.path + cloned_sprite.id = original_sprite.id + cloned_sprite.parentId = original_sprite.parentId + cloned_sprite.displayName = original_sprite.displayName + + # Visual properties + cloned_sprite.offset = original_sprite.offset + cloned_sprite.z = original_sprite.z + cloned_sprite.position = original_sprite.position + + # Physics properties + cloned_sprite.dragSpeed = original_sprite.dragSpeed + cloned_sprite.xFrq = original_sprite.xFrq + cloned_sprite.xAmp = original_sprite.xAmp + cloned_sprite.yFrq = original_sprite.yFrq + cloned_sprite.yAmp = original_sprite.yAmp + cloned_sprite.rFrq = original_sprite.rFrq + cloned_sprite.rAmp = original_sprite.rAmp + cloned_sprite.rdragStr = original_sprite.rdragStr + cloned_sprite.rLimitMin = original_sprite.rLimitMin + cloned_sprite.rLimitMax = original_sprite.rLimitMax + cloned_sprite.stretchAmount = original_sprite.stretchAmount + cloned_sprite.ignoreBounce = original_sprite.ignoreBounce + + # Animation properties + cloned_sprite.frames = original_sprite.frames + cloned_sprite.animSpeed = original_sprite.animSpeed + + # Visibility properties + cloned_sprite.showOnTalk = original_sprite.showOnTalk + cloned_sprite.showOnBlink = original_sprite.showOnBlink + cloned_sprite.costumeLayers = original_sprite.costumeLayers.duplicate() + cloned_sprite.spriteOpacity = original_sprite.spriteOpacity + cloned_sprite.affectChildrenOpacity = original_sprite.affectChildrenOpacity + cloned_sprite.blendMode = original_sprite.blendMode + cloned_sprite.toggle = original_sprite.toggle + cloned_sprite.clipped = original_sprite.clipped + + # Transform properties + cloned_sprite.staticRotation = original_sprite.staticRotation + cloned_sprite.mirrorHorizontal = original_sprite.mirrorHorizontal + cloned_sprite.mirrorVertical = original_sprite.mirrorVertical + + # Wobble sync + cloned_sprite.wobbleSyncGroup = original_sprite.wobbleSyncGroup + + # Add to origin + origin.add_child(cloned_sprite) + + # Apply current costume visibility + if Global.main: + var current_costume = Global.main.costume + if cloned_sprite.costumeLayers[current_costume - 1] == 1: + cloned_sprite.visible = true + else: + cloned_sprite.visible = false + +## Process function to keep sprites in sync with main scene +func _process(delta): + if !Global.main: + return + + # Sync origin position with bounce from main scene + $OriginMotion.position.y = 360 + Global.main.bounceChange + + # Sync sprites with main scene + var main_sprites = get_tree().get_nodes_in_group("saved") + var avatar_sprites = origin.get_children() + + for i in range(min(main_sprites.size(), avatar_sprites.size())): + sync_sprite_state(main_sprites[i], avatar_sprites[i]) + +## Sync individual sprite state +func sync_sprite_state(main_sprite, avatar_sprite): + if !is_instance_valid(main_sprite) or !is_instance_valid(avatar_sprite): + return + + # Sync position and transform + avatar_sprite.position = main_sprite.position + avatar_sprite.rotation = main_sprite.rotation + avatar_sprite.scale = main_sprite.scale + + # Sync visibility based on costume + if Global.main: + var current_costume = Global.main.costume + if avatar_sprite.costumeLayers[current_costume - 1] == 1: + avatar_sprite.visible = main_sprite.visible + else: + avatar_sprite.visible = false + + # Sync animation frame + if avatar_sprite.has_method("get_sprite") and main_sprite.has_method("get_sprite"): + var avatar_spr = avatar_sprite.get_sprite() + var main_spr = main_sprite.get_sprite() + if avatar_spr and main_spr: + avatar_spr.frame = main_spr.frame + +## Handle sprite visibility toggles +func _on_sprite_visibility_toggle(): + sync_avatar() + +## Sync bounce changes +func _sync_bounce(bounce_change): + $OriginMotion.position.y = 360 + bounce_change diff --git a/main_scenes/NDIAvatarViewport.gd.uid b/main_scenes/NDIAvatarViewport.gd.uid new file mode 100644 index 0000000..9febfeb --- /dev/null +++ b/main_scenes/NDIAvatarViewport.gd.uid @@ -0,0 +1 @@ +uid://dkomsdn2pvjwp diff --git a/main_scenes/NDIAvatarViewport.tscn b/main_scenes/NDIAvatarViewport.tscn new file mode 100644 index 0000000..612df6f --- /dev/null +++ b/main_scenes/NDIAvatarViewport.tscn @@ -0,0 +1,18 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://main_scenes/NDIAvatarViewport.gd" id="1"] + +[node name="SubViewport" type="SubViewport"] +handle_input_locally = false +size = Vector2i(1280, 720) +render_target_update_mode = 4 +script = ExtResource("1") + +[node name="NDIOutput" type="NDIOutput"] +name = "PNGTuber Avatar" +enable_editor_output = true + +[node name="OriginMotion" type="Node2D"] +position = Vector2(640, 360) + +[node name="Origin" type="Node2D" parent="OriginMotion"] diff --git a/main_scenes/NDIOutputManager.gd.uid b/main_scenes/NDIOutputManager.gd.uid new file mode 100644 index 0000000..0684e26 --- /dev/null +++ b/main_scenes/NDIOutputManager.gd.uid @@ -0,0 +1 @@ +uid://c1wo8w4ac848b diff --git a/main_scenes/main.gd b/main_scenes/main.gd index ff80a50..c365eb7 100644 --- a/main_scenes/main.gd +++ b/main_scenes/main.gd @@ -8,7 +8,7 @@ var editMode = true @onready var controlPanel = $ControlPanel @onready var editControls = $EditControls @onready var tutorial = $Tutorial -@onready var spriteViewer = $EditControls/SpriteViewer +@onready var spriteViewer = $EditControls/Container/SpriteViewer @onready var viewerArrows = $ViewerArrows @onready var spriteList = $EditControls/SpriteList @@ -26,6 +26,7 @@ var editMode = true @onready var shadow = $shadowSprite @onready var ndi_output = $NDIOutput +@onready var ndi_avatar_viewport = $NDIAvatarViewport #Scene Reference @@ -156,6 +157,10 @@ func _ready(): origin.position = s*0.5 updateCameraPosition() + # Initialize avatar viewport + if ndi_avatar_viewport: + ndi_avatar_viewport.visible = false # Start disabled + # Test NDI output on startup call_deferred("toggle_ndi") @@ -431,8 +436,17 @@ func add_image(path): Global.spriteList.updateData() + # Sync avatar to NDI viewport + if ndi_avatar_viewport: + call_deferred("sync_avatar_viewport") + Global.pushUpdate("Added new sprite.") +## Sync avatar viewport with current sprite state +func sync_avatar_viewport(): + if ndi_avatar_viewport: + ndi_avatar_viewport.sync_avatar() + #Opens File Dialog func _on_add_button_pressed(): fileDialog.visible = true @@ -575,6 +589,10 @@ func _on_load_dialog_file_selected(path): if sprite.has_method("updateOpacity"): sprite.updateOpacity() + # Sync avatar to NDI viewport + if ndi_avatar_viewport: + ndi_avatar_viewport.sync_avatar() + Global.pushUpdate("Loaded avatar at: " + path) onWindowSizeChange() diff --git a/main_scenes/main.tscn b/main_scenes/main.tscn index f835b01..67d1c2e 100644 --- a/main_scenes/main.tscn +++ b/main_scenes/main.tscn @@ -1,6 +1,7 @@ -[gd_scene load_steps=80 format=4 uid="uid://bakgnwotiysu1"] +[gd_scene load_steps=81 format=4 uid="uid://bakgnwotiysu1"] [ext_resource type="Script" uid="uid://c4g7pgn40uv4q" path="res://main_scenes/main.gd" id="1_06alx"] +[ext_resource type="Script" uid="uid://dkomsdn2pvjwp" path="res://main_scenes/NDIAvatarViewport.gd" id="1_avatar_viewport"] [ext_resource type="Script" uid="uid://18kfbk72spvo" path="res://main_scenes/ControlPanel.gd" id="4_5ygs2"] [ext_resource type="Texture2D" uid="uid://goff8ioh380e" path="res://ui_scenes/button sprites/buttonBack.png" id="4_xenui"] [ext_resource type="Texture2D" uid="uid://da4jthat2oas4" path="res://ui_scenes/button sprites/fancy.png" id="5_75a7k"] @@ -14755,7 +14756,21 @@ z_index = -4096 [node name="NDIOutput" type="NDIOutput" parent="."] name = "PNGTuber-Plus" -audio_bus = &"Master" + +[node name="NDIAvatarViewport" type="SubViewport" parent="."] +handle_input_locally = false +size = Vector2i(1280, 720) +render_target_update_mode = 4 +script = ExtResource("1_avatar_viewport") + +[node name="NDIOutput" type="NDIOutput" parent="NDIAvatarViewport"] +name = "PNGTuber Avatar" +enable_editor_output = true + +[node name="OriginMotion" type="Node2D" parent="NDIAvatarViewport"] +position = Vector2(640, 360) + +[node name="Origin" type="Node2D" parent="NDIAvatarViewport/OriginMotion"] [connection signal="pressed" from="ControlPanel/MicButtong/Button" to="." method="_on_button_pressed"] [connection signal="pressed" from="ControlPanel/SettingsButton/settingsButtons" to="." method="_on_settings_buttons_pressed"] diff --git a/ui_scenes/settings/settings_menu.gd b/ui_scenes/settings/settings_menu.gd index a61d89b..19a49b1 100644 --- a/ui_scenes/settings/settings_menu.gd +++ b/ui_scenes/settings/settings_menu.gd @@ -332,3 +332,21 @@ func setup_ndi_settings(): if Global.main.ndi_output: $NDIOutput/ndiSourceName.text = Global.main.ndi_output.name update_ndi_status() + + # Initialize avatar-only checkbox state + $NDIOutput/avatarOnlyCheck.button_pressed = false + if Global.main.ndi_avatar_viewport: + $NDIOutput/avatarOnlyCheck.button_pressed = Global.main.ndi_avatar_viewport.visible + +func _on_avatar_only_check_toggled(toggled_on: bool): + # Enable/disable avatar-only NDI streaming + if Global.main.ndi_avatar_viewport: + Global.main.ndi_avatar_viewport.visible = toggled_on + if toggled_on: + # Sync avatar when enabling + Global.main.ndi_avatar_viewport.sync_avatar() + print("Avatar-only NDI stream enabled") + Global.pushUpdate("Avatar-only NDI stream enabled") + else: + print("Avatar-only NDI stream disabled") + Global.pushUpdate("Avatar-only NDI stream disabled") diff --git a/ui_scenes/settings/settings_menu.tscn b/ui_scenes/settings/settings_menu.tscn index d5f2083..4326b71 100644 --- a/ui_scenes/settings/settings_menu.tscn +++ b/ui_scenes/settings/settings_menu.tscn @@ -14368,6 +14368,14 @@ offset_bottom = 123.0 text = "Status: Not streaming" metadata/_edit_use_anchors_ = true +[node name="avatarOnlyCheck" type="CheckBox" parent="NDIOutput"] +offset_left = 4.0 +offset_top = 125.0 +offset_right = 150.0 +offset_bottom = 149.0 +text = "Avatar-only stream" +metadata/_edit_use_anchors_ = true + [node name="bounceOnCostume" type="Node2D" parent="."] position = Vector2(-172, 122) @@ -14938,6 +14946,7 @@ metadata/_edit_use_anchors_ = true [connection signal="toggled" from="AntiAliasing/CheckBox" to="." method="_on_check_box_toggled"] [connection signal="toggled" from="NDIOutput/ndiEnabledCheck" to="." method="_on_ndi_enabled_check_toggled"] [connection signal="text_changed" from="NDIOutput/ndiSourceName" to="." method="_on_ndi_source_name_text_changed"] +[connection signal="toggled" from="NDIOutput/avatarOnlyCheck" to="." method="_on_avatar_only_check_toggled"] [connection signal="toggled" from="bounceOnCostume/costumeCheck" to="." method="_on_costume_check_toggled"] [connection signal="value_changed" from="BounceForce/bounceForce" to="." method="_on_bounce_force_value_changed"] [connection signal="value_changed" from="BounceGravity/bounceGravity" to="." method="_on_bounce_gravity_value_changed"]