diff --git a/.claude/settings.local.json b/.claude/settings.local.json index d4489992..a9bed0fc 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6722fe768e744cfd4a3e35c1d77d853e7d495a6ec5e05b15e449ed77a075e404 -size 372 +oid sha256:df1b49d2596a7ac72ace646240bc72d0e197dbcd029a128988893b594b8d06d7 +size 678 diff --git a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs index 2a21cfd2..1d7c0992 100644 --- a/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs +++ b/Assets/External/Ifacialmocap/UnityRecieve_FACEMOTION3D_and_iFacialMocap.cs @@ -493,6 +493,35 @@ public class UnityRecieve_FACEMOTION3D_and_iFacialMocap : MonoBehaviour } } + /// + /// 페이셜 모션 캡처 재접속 + /// + public void Reconnect() + { + Debug.Log("[iFacialMocap] 재접속 시도 중..."); + + try + { + // 기존 연결 종료 + StopUDP(); + + // 잠시 대기 + Thread.Sleep(500); + + // 플래그 리셋 + StartFlag = true; + + // 재시작 + StartFunction(); + + Debug.Log("[iFacialMocap] 재접속 완료"); + } + catch (Exception e) + { + Debug.LogError($"[iFacialMocap] 재접속 실패: {e.Message}"); + } + } + private bool HasBlendShapes(SkinnedMeshRenderer skin) { if (!skin.sharedMesh) diff --git a/Assets/External/websocket-sharp/websocket-sharp.csproj.meta b/Assets/External/websocket-sharp/websocket-sharp.csproj.meta new file mode 100644 index 00000000..66af5f92 --- /dev/null +++ b/Assets/External/websocket-sharp/websocket-sharp.csproj.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ae0a68acee725e141b02318f249f7990 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset b/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset index a61368ec..5d96f5cf 100644 --- a/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset +++ b/Assets/Resources/Settings/Streamingle Render Pipeline Asset.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6387b206982a696eb39abcd29ed154075793f81ba50c5e3eb6262fa649c6d22f +oid sha256:46f739375a1dc60bebcc5c6144420007796cf257f7d6005244e02c7aff88cba2 size 4536 diff --git a/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset b/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset index c5b8fde2..cf6a00fa 100644 --- a/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset +++ b/Assets/Resources/Settings/Streamingle Render Pipeline Asset_Renderer.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa2fc5d2364f88b34db86a555b15d2d7a8441c8147e2f321c2a1f56f2c4405a0 +oid sha256:d9bbf57b32f192d0546d35779a845decdfd06e7606877cef9e949399e4e84da0 size 15608 diff --git a/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower/GlobalVolumeProfile.asset b/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower/GlobalVolumeProfile.asset index 9eeae16e..a6d35c7c 100644 --- a/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower/GlobalVolumeProfile.asset +++ b/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower/GlobalVolumeProfile.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef78aed76f0afe7d3354a96c8c7710a7101649cf7acdd200e7a83d125e8a6ca1 -size 5062 +oid sha256:bc002b37f6c6a86fecc28f5bf7fa6e1ec69f3e3dec899224195fc8b81627b116 +size 9323 diff --git a/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower_Night.unity b/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower_Night.unity index 08a7877c..d2ea78ea 100644 --- a/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower_Night.unity +++ b/Assets/ResourcesData/Background/Greenhouse Garden/Scene/Greenhouse Flower_Night.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f559542a90891e4c25240ebaa1c915712eb2caf7ae7a8170da958dfc76e6d951 -size 161874 +oid sha256:a6cb0e6ab1df8f59b84bdf2f1aac1909f36ac8426d0670a338e74f7da8415bca +size 161867 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyA_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyA_mtoon.asset index 5d3671e8..d793c82e 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyA_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyA_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85a5dedba2f051414b336fab378ecda210114c95004de047c4d2decba8733d5f -size 2970 +oid sha256:3616db7a90f0c629473ec9247095d9c61b8f69a951acccf31d9dd65b7fa1c243 +size 47935 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_eyepatch_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_eyepatch_mtoon.asset index 80baf1f5..f2b73de5 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_eyepatch_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_eyepatch_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5dd105b40dbb1c91015c21f7f1522b209a092961d210856b9894b8324e606f8f -size 3028 +oid sha256:f1eac9a93c15183581e481cd735dcc90df58c4a7c75d712b3d7f9cdc87f2fb85 +size 48030 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_mtoon.asset index ed3af0d2..4745fc82 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/bodyB_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2d00d740c35dfdf4bae32883e79d190e8e7141521372267a71216e9c13d2bc65 -size 3019 +oid sha256:83f8971f03ad8329aa2664c21441e00bfefa5a41b089423aed9c77752c5bc8fc +size 48016 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionA_NoOutline_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionA_NoOutline_mtoon.asset index babb5ed0..8bceefb0 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionA_NoOutline_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionA_NoOutline_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c3a7613b1b12872bea604f737fb90223e607d257fd849fa907da30e2176e57f8 -size 3005 +oid sha256:98e10692cde69b3aa34b41fd25c8da6f207376777228f3efabe0917d11e5d23a +size 47992 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionB_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionB_mtoon.asset index b5f123a1..38a0669b 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionB_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/emissionB_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3ead0d12c08fbaf18f85b122f6309591d9684fb56e3485b33d2cd1c801e2f02 -size 3053 +oid sha256:982c08da61aa1d0ad4abb2154c1c9cee4e40a1a453390419a7eabd8b9ef9f64c +size 48039 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeGuruguru_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeGuruguru_mtoon.asset index 61f47fca..4c3f4460 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeGuruguru_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeGuruguru_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3daa51a43b7f73e9e174223f1024217b41b14a6cbd486943b225facec7e6240f -size 2918 +oid sha256:a64c7ba2a2c829e1b93d8d1278560ce153485dcc93cecbc1282842e48be3acfd +size 47903 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeKakusei_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeKakusei_mtoon.asset index 578a2fc4..e9b9cd96 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeKakusei_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eyeKakusei_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:27445feac16a77965cf40899dd46408c98ab7f9ccad3120d2959847fde7ee5a4 -size 2917 +oid sha256:2f9096c89e10287f679dcde65fe33fb261a8550ddb8270dc4fd457278a661fcf +size 47902 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eye_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eye_mtoon.asset index 571d1031..42f4c4b3 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eye_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/eye_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4f18b6308d3766e6ae9b22b0344849b3a4e07130764210b2727ec9307698e4d7 -size 2910 +oid sha256:3ca44d7aa5b2c9844179907f2b15fef9633c8dec881a3abc66f7491f0c7ff09d +size 47895 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/face_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/face_mtoon.asset index e63846e6..81c12baa 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/face_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/face_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a46415ffccfae46c17c3a929ef26dbb000ac2f9e66f0c8680aa52a72d056ebe -size 3024 +oid sha256:e6d3f766731f46ae44431d113cd8132d1dae655a7a77d3b9ef7b725c53b497f0 +size 48084 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowA_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowA_mtoon.asset index 504e28ef..e31a1c55 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowA_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowA_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2769f403fafd6dea079109668d377d57970b099db0eafb9b2e778eaeb1599c61 -size 3049 +oid sha256:eb45995a7e31b839dbce3382b7a6985b0e90ec60153bc63853a37de56333d546 +size 48035 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowB_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowB_mtoon.asset index 31edb5ea..400052f7 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowB_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/glowB_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95c26b511121e6d34176e034dd960f864b2820981ece6d1906ed67e1f6f505a4 -size 3049 +oid sha256:e65ec600f65d26845f5e2434fd081a20ae48846a3cc7aeffc5f406681574adc9 +size 48035 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/hair_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/hair_mtoon.asset index 3ea2655c..01c09e0c 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/hair_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/hair_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4654c7df8ec0fccd34c0b984bc24b7419c6e0402949ae1582b7cadab62854c00 -size 3023 +oid sha256:0ee48c62afde594226d3a28da0af1566969e0dce821e02d615aa763556f61d3e +size 48072 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/skinB_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/skinB_mtoon.asset index dc99acbc..9bbee8c0 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/skinB_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/skinB_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:15b49159f7443c85f48af32de5d92c3bb6bae5a36718e1f6ec0bd4a7bdaa0583 -size 3024 +oid sha256:a98185be5834ccc3d6e0bd248d4cb7ea45bbae63a5dbed9141d836cb637d0ed7 +size 48073 diff --git a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/thunder_mtoon.asset b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/thunder_mtoon.asset index 98d7e523..bb3c2d7b 100644 --- a/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/thunder_mtoon.asset +++ b/Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.Materials/thunder_mtoon.asset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b3d9d120b40de97adc0c567bf14a3463ed2e0478ee610ee4fab96164edba7c91 -size 2914 +oid sha256:d5a428c3f8f9470c74d950ee909e87f07f1a553b7c9d353eb3b89660506526d5 +size 47887 diff --git a/Assets/Scripts/Editor/StreamingleControllerSetupToolAdvanced.cs b/Assets/Scripts/Editor/StreamingleControllerSetupToolAdvanced.cs index 371ee544..5f9c1d6b 100644 --- a/Assets/Scripts/Editor/StreamingleControllerSetupToolAdvanced.cs +++ b/Assets/Scripts/Editor/StreamingleControllerSetupToolAdvanced.cs @@ -10,6 +10,7 @@ namespace Streamingle.Editor private bool createEventController = true; private bool createStreamDeckManager = true; private bool createAvatarOutfitController = true; + private bool createSystemController = true; private string parentObjectName = "Streamingle 컨트롤러들"; private bool createParentObject = true; @@ -26,6 +27,7 @@ namespace Streamingle.Editor private ItemController existingItemController; private EventController existingEventController; private AvatarOutfitController existingAvatarOutfitController; + private SystemController existingSystemController; [MenuItem("Tools/Streamingle/고급 컨트롤러 설정 도구")] public static void ShowWindow() @@ -65,6 +67,7 @@ namespace Streamingle.Editor GUILayout.Label("생성할 컨트롤러들", EditorStyles.boldLabel); createStreamDeckManager = EditorGUILayout.Toggle("StreamDeck 서버 매니저", createStreamDeckManager); + createSystemController = EditorGUILayout.Toggle("시스템 컨트롤러", createSystemController); createCameraManager = EditorGUILayout.Toggle("카메라 매니저", createCameraManager); createItemController = EditorGUILayout.Toggle("아이템 컨트롤러", createItemController); createEventController = EditorGUILayout.Toggle("이벤트 컨트롤러", createEventController); @@ -198,7 +201,20 @@ namespace Streamingle.Editor EditorGUILayout.LabelField("✗ 발견되지 않음", EditorStyles.boldLabel); } EditorGUILayout.EndHorizontal(); - + + EditorGUILayout.BeginHorizontal(); + GUILayout.Label("시스템 컨트롤러:", GUILayout.Width(200)); + if (existingSystemController != null) + { + string parentInfo = GetParentInfo(existingSystemController.transform); + EditorGUILayout.LabelField($"✓ 발견됨 {parentInfo}", EditorStyles.boldLabel); + } + else + { + EditorGUILayout.LabelField("✗ 발견되지 않음", EditorStyles.boldLabel); + } + EditorGUILayout.EndHorizontal(); + EditorGUILayout.EndVertical(); } @@ -221,6 +237,7 @@ namespace Streamingle.Editor existingItemController = FindObjectOfType(); existingEventController = FindObjectOfType(); existingAvatarOutfitController = FindObjectOfType(); + existingSystemController = FindObjectOfType(); } private void CreateControllers() @@ -239,7 +256,13 @@ namespace Streamingle.Editor { CreateStreamDeckManager(parentObject); } - + + // System Controller 생성 + if (createSystemController && existingSystemController == null) + { + CreateSystemController(parentObject); + } + // Camera Manager 생성 if (createCameraManager && existingCameraManager == null) { @@ -337,7 +360,15 @@ namespace Streamingle.Editor movedCount++; UnityEngine.Debug.Log($"아바타 의상 컨트롤러를 {parent.name} 하위로 이동"); } - + + // System Controller 이동 + if (existingSystemController != null && existingSystemController.transform.parent != parent.transform) + { + existingSystemController.transform.SetParent(parent.transform); + movedCount++; + UnityEngine.Debug.Log($"시스템 컨트롤러를 {parent.name} 하위로 이동"); + } + if (movedCount > 0) { UnityEngine.Debug.Log($"{movedCount}개의 컨트롤러를 {parent.name} 하위로 이동했습니다."); @@ -403,9 +434,19 @@ namespace Streamingle.Editor avatarOutfitControllerProperty.objectReferenceValue = existingAvatarOutfitController; } } - + + // System Controller 연결 + if (existingSystemController != null) + { + var systemControllerProperty = serializedObject.FindProperty("systemController"); + if (systemControllerProperty != null) + { + systemControllerProperty.objectReferenceValue = existingSystemController; + } + } + serializedObject.ApplyModifiedProperties(); - + UnityEngine.Debug.Log("기존 컨트롤러들을 StreamDeck 서버 매니저에 연결했습니다!"); } @@ -563,8 +604,51 @@ namespace Streamingle.Editor } serializedObject.ApplyModifiedProperties(); - + UnityEngine.Debug.Log("아바타 의상 컨트롤러 생성됨"); } + + private void CreateSystemController(GameObject parent) + { + GameObject systemControllerObject = new GameObject("시스템 컨트롤러"); + + if (parent != null) + { + systemControllerObject.transform.SetParent(parent.transform); + } + + // SystemController 스크립트 추가 + var systemController = systemControllerObject.AddComponent(); + + // 기본 설정 + SerializedObject serializedObject = new SerializedObject(systemController); + serializedObject.Update(); + + // OptiTrack 클라이언트 자동 찾기는 Start()에서 수행됨 + // Motion Recorder 자동 찾기 활성화 + var autoFindRecordersProperty = serializedObject.FindProperty("autoFindRecorders"); + if (autoFindRecordersProperty != null) + { + autoFindRecordersProperty.boolValue = true; + } + + // Facial Motion 클라이언트 자동 찾기 활성화 + var autoFindFacialMotionClientsProperty = serializedObject.FindProperty("autoFindFacialMotionClients"); + if (autoFindFacialMotionClientsProperty != null) + { + autoFindFacialMotionClientsProperty.boolValue = true; + } + + // 디버그 로그 활성화 + var enableDebugLogProperty = serializedObject.FindProperty("enableDebugLog"); + if (enableDebugLogProperty != null) + { + enableDebugLogProperty.boolValue = true; + } + + serializedObject.ApplyModifiedProperties(); + + UnityEngine.Debug.Log("시스템 컨트롤러 생성됨"); + } } } \ No newline at end of file diff --git a/Assets/Scripts/SpoutOutputScript/FinalOutputShader.mat b/Assets/Scripts/SpoutOutputScript/FinalOutputShader.mat index 0547125d..03182bd2 100644 --- a/Assets/Scripts/SpoutOutputScript/FinalOutputShader.mat +++ b/Assets/Scripts/SpoutOutputScript/FinalOutputShader.mat @@ -147,7 +147,7 @@ Material: - _BaseColor: {r: 1, g: 1, b: 1, a: 1} - _Color: {r: 1, g: 1, b: 1, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - - _Resolution: {r: 1101, g: 514, b: 0, a: 0} + - _Resolution: {r: 1920, g: 1080, b: 0, a: 0} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} m_BuildTextureStacks: [] m_AllowLocking: 1 diff --git a/Assets/Scripts/SpoutOutputScript/FinalOutputShader.shadergraph b/Assets/Scripts/SpoutOutputScript/FinalOutputShader.shadergraph index e45f8220..b629ce53 100644 --- a/Assets/Scripts/SpoutOutputScript/FinalOutputShader.shadergraph +++ b/Assets/Scripts/SpoutOutputScript/FinalOutputShader.shadergraph @@ -157,6 +157,20 @@ "m_SlotId": 1 } }, + { + "m_OutputSlot": { + "m_Node": { + "m_Id": "a545499797a14671a5d598434d7c85bc" + }, + "m_SlotId": 4 + }, + "m_InputSlot": { + "m_Node": { + "m_Id": "ad863a97e6f14704b6137da83d1dba35" + }, + "m_SlotId": 0 + } + }, { "m_OutputSlot": { "m_Node": { @@ -346,6 +360,8 @@ "overrideHLSLDeclaration": false, "hlslDeclarationOverride": 0, "m_Hidden": false, + "m_PerRendererData": false, + "m_customAttributes": [], "m_Value": { "x": 1920.0, "y": 1080.0, @@ -465,12 +481,15 @@ "overrideHLSLDeclaration": true, "hlslDeclarationOverride": 2, "m_Hidden": false, + "m_PerRendererData": false, + "m_customAttributes": [], "m_Value": { - "m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}", + "m_SerializedTexture": "", "m_Guid": "" }, "isMainTexture": false, "useTilingAndOffset": false, + "useTexelSize": true, "m_Modifiable": true, "m_DefaultType": 0 } @@ -759,9 +778,9 @@ "m_Position": { "serializedVersion": "2", "x": -1113.0, - "y": 287.0, + "y": 301.0, "width": 145.0, - "height": 129.00003051757813 + "height": 128.0 } }, "m_Slots": [ @@ -845,7 +864,7 @@ "m_StageCapability": 3, "m_BareResource": false, "m_Texture": { - "m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}", + "m_SerializedTexture": "", "m_Guid": "" }, "m_DefaultType": 0 @@ -1256,7 +1275,7 @@ "m_StageCapability": 3, "m_BareResource": false, "m_Texture": { - "m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}", + "m_SerializedTexture": "", "m_Guid": "" }, "m_DefaultType": 0 @@ -1321,10 +1340,10 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": -848.0000610351563, - "y": 365.0000305175781, + "x": -851.0, + "y": 364.9999694824219, "width": 281.0, - "height": 190.00003051757813 + "height": 190.00009155273438 } }, "m_Slots": [ @@ -1360,7 +1379,8 @@ }, "m_SourceType": 0, "m_FunctionName": "AAFromGreenChannel", - "m_FunctionSource": "f61580200f158d84880df345e2130e9b", + "m_FunctionSource": "7f3a4e8b9c2d1a5e6f8b9c2d1a5e6f8b", + "m_FunctionSourceUsePragmas": true, "m_FunctionBody": "Enter function body here..." } @@ -1609,10 +1629,10 @@ "m_Expanded": true, "m_Position": { "serializedVersion": "2", - "x": -1099.0001220703125, - "y": 569.0000610351563, - "width": 131.00006103515626, - "height": 33.99993896484375 + "x": -1099.0, + "y": 498.0, + "width": 131.0, + "height": 34.0 } }, "m_Slots": [ @@ -1646,8 +1666,8 @@ "m_Position": { "serializedVersion": "2", "x": -1145.0, - "y": 444.0000305175781, - "width": 176.99993896484376, + "y": 429.0, + "width": 177.0, "height": 34.0 } }, @@ -1719,12 +1739,15 @@ "overrideHLSLDeclaration": true, "hlslDeclarationOverride": 1, "m_Hidden": false, + "m_PerRendererData": false, + "m_customAttributes": [], "m_Value": { - "m_SerializedTexture": "{\"texture\":{\"instanceID\":0}}", + "m_SerializedTexture": "", "m_Guid": "" }, "isMainTexture": false, "useTilingAndOffset": false, + "useTexelSize": true, "m_Modifiable": true, "m_DefaultType": 1 } @@ -1763,8 +1786,8 @@ "m_Position": { "serializedVersion": "2", "x": -1089.0, - "y": 521.0000610351563, - "width": 120.99993896484375, + "y": 464.0, + "width": 121.0, "height": 34.0 } }, @@ -1873,12 +1896,24 @@ "overrideHLSLDeclaration": false, "hlslDeclarationOverride": 0, "m_Hidden": false, + "m_PerRendererData": false, + "m_customAttributes": [], "m_Value": 0.0, "m_FloatType": 0, "m_RangeValues": { "x": 0.0, "y": 1.0 - } + }, + "m_SliderType": 0, + "m_SliderPower": 3.0, + "m_EnumType": 0, + "m_CSharpEnumString": "", + "m_EnumNames": [ + "Default" + ], + "m_EnumValues": [ + 0 + ] } { diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl b/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl new file mode 100644 index 00000000..5e28b442 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl @@ -0,0 +1,115 @@ +#ifndef AA_FROM_GREEN_CHANNEL_INCLUDED +#define AA_FROM_GREEN_CHANNEL_INCLUDED + +// Shader Graph Custom Function +// UnityTexture2D와 UnitySamplerState를 받아서 G 채널에 안티에일리어싱 적용 + +void AAFromGreenChannel_float( + float2 UV, + UnityTexture2D Tex, + UnitySamplerState Samp, + float Strength, + float2 Resolution, + out float New) +{ + // Texel Size 계산 + float2 texelSize = 1.0 / Resolution; + + // 중앙값 + float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g; + + if (Strength > 0.0) + { + // 주변 8방향 샘플링 + float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g; + float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g; + float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g; + float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g; + + float topLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, texelSize.y)).g; + float topRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, texelSize.y)).g; + float bottomLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, -texelSize.y)).g; + float bottomRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, -texelSize.y)).g; + + // 9탭 박스 필터 평균 + float average = (center + left + right + top + bottom + + topLeft + topRight + bottomLeft + bottomRight) / 9.0; + + // Strength에 따라 블렌딩 + New = lerp(center, average, saturate(Strength)); + } + else + { + New = center; + } +} + +// Simple 버전 - 5탭 크로스 필터 +void AAFromGreenChannelSimple_float( + float2 UV, + UnityTexture2D Tex, + UnitySamplerState Samp, + float Strength, + float2 Resolution, + out float New) +{ + float2 texelSize = 1.0 / Resolution; + + float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g; + + if (Strength > 0.0) + { + float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g; + float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g; + float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g; + float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g; + + float average = (center + left + right + top + bottom) / 5.0; + + New = lerp(center, average, saturate(Strength)); + } + else + { + New = center; + } +} + +// Weighted 버전 - 가우시안 가중치 +void AAFromGreenChannelWeighted_float( + float2 UV, + UnityTexture2D Tex, + UnitySamplerState Samp, + float Strength, + float2 Resolution, + out float New) +{ + float2 texelSize = 1.0 / Resolution; + + float center = SAMPLE_TEXTURE2D(Tex, Samp, UV).g; + + if (Strength > 0.0) + { + float left = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, 0)).g; + float right = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, 0)).g; + float top = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, texelSize.y)).g; + float bottom = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(0, -texelSize.y)).g; + + float topLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, texelSize.y)).g; + float topRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, texelSize.y)).g; + float bottomLeft = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(-texelSize.x, -texelSize.y)).g; + float bottomRight = SAMPLE_TEXTURE2D(Tex, Samp, UV + float2(texelSize.x, -texelSize.y)).g; + + // 가우시안 가중치 + float weighted = center * 0.25; + weighted += (left + right + top + bottom) * 0.125; + weighted += (topLeft + topRight + bottomLeft + bottomRight) * 0.0625; + + New = lerp(center, weighted, saturate(Strength)); + } + else + { + New = center; + } +} + +#endif // AA_FROM_GREEN_CHANNEL_INCLUDED diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl.meta b/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl.meta new file mode 100644 index 00000000..aa7e4415 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/AAFromGreenChannel.hlsl.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7f3a4e8b9c2d1a5e6f8b9c2d1a5e6f8b +ShaderIncludeImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader b/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader new file mode 100644 index 00000000..6d9316d1 --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader @@ -0,0 +1,93 @@ +Shader "Hidden/AlphaFromNiloToon" +{ + Properties + { + _MainTex ("Main Texture", 2D) = "white" {} + _AlphaTex ("Alpha Texture (NiloToon Prepass)", 2D) = "white" {} + _BlurRadius ("Blur Radius", Float) = 1.0 + } + + SubShader + { + Tags { "RenderType"="Transparent" "Queue"="Transparent" } + LOD 100 + + ZWrite Off + Blend SrcAlpha OneMinusSrcAlpha + + Pass + { + CGPROGRAM + #pragma vertex vert + #pragma fragment frag + #include "UnityCG.cginc" + + struct appdata + { + float4 vertex : POSITION; + float2 uv : TEXCOORD0; + }; + + struct v2f + { + float2 uv : TEXCOORD0; + float4 vertex : SV_POSITION; + }; + + sampler2D _MainTex; + sampler2D _AlphaTex; + float4 _MainTex_ST; + float4 _AlphaTex_TexelSize; + float _BlurRadius; + + v2f vert (appdata v) + { + v2f o; + o.vertex = UnityObjectToClipPos(v.vertex); + o.uv = TRANSFORM_TEX(v.uv, _MainTex); + return o; + } + + // 3x3 가우시안 블러 커널 (매우 가벼운 블러) + fixed GaussianBlurAlpha(float2 uv) + { + float2 texelSize = _AlphaTex_TexelSize.xy * _BlurRadius; + + // 가우시안 가중치 (3x3 커널) + fixed alpha = 0.0; + + // 중앙 픽셀 (가장 높은 가중치) + alpha += tex2D(_AlphaTex, uv).g * 0.25; + + // 상하좌우 (중간 가중치) + alpha += tex2D(_AlphaTex, uv + float2(0, texelSize.y)).g * 0.125; + alpha += tex2D(_AlphaTex, uv + float2(0, -texelSize.y)).g * 0.125; + alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, 0)).g * 0.125; + alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, 0)).g * 0.125; + + // 대각선 (낮은 가중치) + alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, texelSize.y)).g * 0.0625; + alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, texelSize.y)).g * 0.0625; + alpha += tex2D(_AlphaTex, uv + float2(texelSize.x, -texelSize.y)).g * 0.0625; + alpha += tex2D(_AlphaTex, uv + float2(-texelSize.x, -texelSize.y)).g * 0.0625; + + return alpha; + } + + fixed4 frag (v2f i) : SV_Target + { + // 메인 텍스처에서 RGB 가져오기 + fixed4 col = tex2D(_MainTex, i.uv); + + // NiloToon Prepass 버퍼의 G 채널에서 알파 가져오기 (가우시안 블러 적용) + fixed alpha = GaussianBlurAlpha(i.uv); + + // RGB는 그대로, 알파는 블러 처리된 값 사용 + col.a = alpha; + + return col; + } + ENDCG + } + } +} diff --git a/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader.meta b/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader.meta new file mode 100644 index 00000000..14ab3fde --- /dev/null +++ b/Assets/Scripts/SpoutOutputScript/Shaders/AlphaFromNiloToon.shader.meta @@ -0,0 +1,9 @@ +fileFormatVersion: 2 +guid: bac6acd56892cc94ba00d208d4d82712 +ShaderImporter: + externalObjects: {} + defaultTextures: [] + nonModifiableTextures: [] + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs index 7eb7dfc5..a32534bf 100644 --- a/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs +++ b/Assets/Scripts/Streamdeck/StreamDeckServerManager.cs @@ -16,6 +16,7 @@ public class StreamDeckServerManager : MonoBehaviour public ItemController itemController { get; private set; } public EventController eventController { get; private set; } public AvatarOutfitController avatarOutfitController { get; private set; } + public SystemController systemController { get; private set; } // 싱글톤 패턴으로 StreamDeckService에서 접근 가능하도록 public static StreamDeckServerManager Instance { get; private set; } @@ -60,6 +61,13 @@ public class StreamDeckServerManager : MonoBehaviour Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다."); } + // SystemController 찾기 + systemController = FindObjectOfType(); + if (systemController == null) + { + Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다."); + } + StartServer(); } @@ -335,6 +343,23 @@ public class StreamDeckServerManager : MonoBehaviour HandleGetAvatarOutfitList(service); break; + // SystemController 명령어들 + case "toggle_optitrack_markers": + case "show_optitrack_markers": + case "hide_optitrack_markers": + case "reconnect_optitrack": + case "reconnect_facial_motion": + case "refresh_facial_motion_clients": + case "start_motion_recording": + case "stop_motion_recording": + case "toggle_motion_recording": + case "refresh_motion_recorders": + case "capture_screenshot": + case "capture_alpha_screenshot": + case "open_screenshot_folder": + HandleSystemCommand(message); + break; + case "test": // 테스트 메시지 에코 응답 var response = new @@ -961,6 +986,43 @@ public class StreamDeckServerManager : MonoBehaviour } } + private void HandleSystemCommand(Dictionary message) + { + string messageType = message.ContainsKey("type") ? message["type"].ToString() : null; + Debug.Log($"[StreamDeckServerManager] 시스템 명령어 실행: {messageType}"); + + if (systemController == null) + { + Debug.LogError("[StreamDeckServerManager] SystemController가 null입니다!"); + return; + } + + try + { + // 파라미터 추출 (있을 경우) + Dictionary parameters = new Dictionary(); + + if (message.ContainsKey("data")) + { + var dataObject = message["data"]; + if (dataObject is Newtonsoft.Json.Linq.JObject jObject) + { + foreach (var prop in jObject.Properties()) + { + parameters[prop.Name] = prop.Value.ToString(); + } + } + } + + // SystemController의 ExecuteCommand 호출 + systemController.ExecuteCommand(messageType, parameters); + } + catch (Exception ex) + { + Debug.LogError($"[StreamDeckServerManager] 시스템 명령어 실행 실패: {ex.Message}"); + } + } + private void HandleGetAvatarOutfitList(StreamDeckService service) { @@ -1009,7 +1071,9 @@ public class StreamDeckService : WebSocketBehavior protected override void OnMessage(WebSocketSharp.MessageEventArgs e) { - Debug.Log($"[StreamDeckService] 원본 메시지 수신: {e.Data}"); + string timestamp = System.DateTime.Now.ToString("HH:mm:ss.fff"); + Debug.Log($"[{timestamp}] [StreamDeckService] 원본 메시지 수신: {e.Data}"); + Debug.Log($"[{timestamp}] [StreamDeckService] ⚠️ 지금 바로 버튼을 클릭했나요? 클릭했다면 어떤 버튼인지 확인하세요!"); // 메인 스레드에서 처리하도록 매니저에게 전달 if (serverManager != null) diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs new file mode 100644 index 00000000..82ebb553 --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs @@ -0,0 +1,773 @@ +using UnityEngine; +using System.Collections.Generic; +using System.Linq; +using System.IO; +using System; +using Entum; + +/// +/// StreamDeck 단일 기능 버튼들을 통합 관리하는 시스템 컨트롤러 +/// 각 기능은 고유 ID로 식별되며, 확장이 용이한 구조 +/// +public class SystemController : MonoBehaviour +{ + [Header("OptiTrack 참조")] + public OptitrackStreamingClient optitrackClient; + + [Header("모션 녹화 설정")] + [Tooltip("모션 녹화 시 OptiTrack Motive도 함께 녹화할지 여부")] + public bool recordOptiTrackWithMotion = true; + + [Header("EasyMotion Recorder")] + [Tooltip("true면 씬의 모든 MotionDataRecorder를 자동으로 찾습니다")] + public bool autoFindRecorders = true; + + [Tooltip("수동으로 지정할 레코더 목록 (autoFindRecorders가 false일 때 사용)")] + public List motionRecorders = new List(); + + [Header("Facial Motion Capture")] + [Tooltip("true면 씬의 모든 페이셜 모션 클라이언트를 자동으로 찾습니다")] + public bool autoFindFacialMotionClients = true; + + [Tooltip("수동으로 지정할 페이셜 모션 클라이언트 목록 (autoFindFacialMotionClients가 false일 때 사용)")] + public List facialMotionClients = new List(); + + [Header("Screenshot Settings")] + [Tooltip("스크린샷 해상도 (기본: 4K)")] + public int screenshotWidth = 3840; + + [Tooltip("스크린샷 해상도 (기본: 4K)")] + public int screenshotHeight = 2160; + + [Tooltip("스크린샷 저장 경로 (비어있으면 바탕화면)")] + public string screenshotSavePath = ""; + + [Tooltip("파일명 앞에 붙을 접두사")] + public string screenshotFilePrefix = "Screenshot"; + + [Tooltip("알파 채널 추출용 셰이더")] + public Shader alphaShader; + + [Tooltip("NiloToon Prepass 버퍼 텍스처 이름")] + public string niloToonPrepassBufferName = "_NiloToonPrepassBufferTex"; + + [Tooltip("촬영할 카메라 (비어있으면 메인 카메라 사용)")] + public Camera screenshotCamera; + + [Tooltip("알파 채널 블러 반경 (0 = 블러 없음, 1.0 = 약한 블러)")] + [Range(0f, 3f)] + public float alphaBlurRadius = 1.0f; + + [Header("디버그")] + public bool enableDebugLog = true; + + private bool isRecording = false; + private Material alphaMaterial; + + // 싱글톤 패턴 + public static SystemController Instance { get; private set; } + + private void Awake() + { + if (Instance == null) + { + Instance = this; + } + else + { + Destroy(gameObject); + } + } + + private void Start() + { + // OptiTrack 클라이언트 자동 찾기 + if (optitrackClient == null) + { + optitrackClient = FindObjectOfType(); + } + + // Motion Recorder 자동 찾기 + if (autoFindRecorders) + { + RefreshMotionRecorders(); + } + + // Facial Motion 클라이언트 자동 찾기 + if (autoFindFacialMotionClients) + { + RefreshFacialMotionClients(); + } + + // Screenshot 설정 초기화 + if (screenshotCamera == null) + { + screenshotCamera = Camera.main; + } + + if (alphaShader == null) + { + alphaShader = Shader.Find("Hidden/AlphaFromNiloToon"); + if (alphaShader == null) + { + LogError("알파 셰이더를 찾을 수 없습니다: Hidden/AlphaFromNiloToon"); + } + } + + if (string.IsNullOrEmpty(screenshotSavePath)) + { + screenshotSavePath = Path.Combine(Application.dataPath, "..", "Screenshots"); + } + + // Screenshots 폴더가 없으면 생성 + if (!Directory.Exists(screenshotSavePath)) + { + Directory.CreateDirectory(screenshotSavePath); + Log($"Screenshots 폴더 생성됨: {screenshotSavePath}"); + } + + Log("SystemController 초기화 완료"); + Log($"Motion Recorder 개수: {motionRecorders.Count}"); + Log($"Facial Motion 클라이언트 개수: {facialMotionClients.Count}"); + Log($"Screenshot 카메라: {(screenshotCamera != null ? "설정됨" : "없음")}"); + } + + /// + /// 씬에서 모든 MotionDataRecorder를 다시 찾습니다 + /// + public void RefreshMotionRecorders() + { + var allRecorders = FindObjectsOfType(); + motionRecorders = allRecorders.ToList(); + Log($"Motion Recorder {motionRecorders.Count}개 발견"); + } + + #region OptiTrack 마커 기능 + + /// + /// OptiTrack 마커 표시 토글 (켜기/끄기) + /// + public void ToggleOptitrackMarkers() + { + if (optitrackClient == null) + { + LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); + return; + } + + optitrackClient.ToggleDrawMarkers(); + Log($"OptiTrack 마커 표시: {optitrackClient.DrawMarkers}"); + } + + /// + /// OptiTrack 마커 표시 켜기 + /// + public void ShowOptitrackMarkers() + { + if (optitrackClient == null) + { + LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); + return; + } + + if (!optitrackClient.DrawMarkers) + { + optitrackClient.ToggleDrawMarkers(); + } + Log("OptiTrack 마커 표시 켜짐"); + } + + /// + /// OptiTrack 마커 표시 끄기 + /// + public void HideOptitrackMarkers() + { + if (optitrackClient == null) + { + LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); + return; + } + + if (optitrackClient.DrawMarkers) + { + optitrackClient.ToggleDrawMarkers(); + } + Log("OptiTrack 마커 표시 꺼짐"); + } + + #endregion + + #region OptiTrack 재접속 기능 + + /// + /// OptiTrack 서버에 재접속 시도 + /// + public void ReconnectOptitrack() + { + if (optitrackClient == null) + { + LogError("OptitrackStreamingClient를 찾을 수 없습니다!"); + return; + } + + Log("OptiTrack 재접속 시도..."); + optitrackClient.Reconnect(); + Log("OptiTrack 재접속 명령 전송 완료"); + } + + /// + /// OptiTrack 연결 상태 확인 + /// + public bool IsOptitrackConnected() + { + if (optitrackClient == null) + { + return false; + } + + return optitrackClient.IsConnected(); + } + + /// + /// OptiTrack 연결 상태를 문자열로 반환 + /// + public string GetOptitrackConnectionStatus() + { + if (optitrackClient == null) + { + return "OptiTrack 클라이언트 없음"; + } + + return optitrackClient.GetConnectionStatus(); + } + + #endregion + + #region Facial Motion Capture 재접속 기능 + + /// + /// 씬에서 모든 Facial Motion 클라이언트를 다시 찾습니다 + /// + public void RefreshFacialMotionClients() + { + var allClients = FindObjectsOfType(); + facialMotionClients = allClients.ToList(); + Log($"Facial Motion 클라이언트 {facialMotionClients.Count}개 발견"); + } + + /// + /// 모든 Facial Motion 클라이언트 재접속 + /// + public void ReconnectFacialMotion() + { + if (autoFindFacialMotionClients) + { + RefreshFacialMotionClients(); + } + + if (facialMotionClients == null || facialMotionClients.Count == 0) + { + LogError("Facial Motion 클라이언트가 없습니다!"); + return; + } + + Log($"Facial Motion 클라이언트 재접속 시도... ({facialMotionClients.Count}개)"); + + int reconnectedCount = 0; + foreach (var client in facialMotionClients) + { + if (client != null) + { + try + { + client.Reconnect(); + reconnectedCount++; + Log($"클라이언트 재접속 성공: {client.gameObject.name}"); + } + catch (System.Exception e) + { + LogError($"클라이언트 재접속 실패 ({client.gameObject.name}): {e.Message}"); + } + } + } + + if (reconnectedCount > 0) + { + Log($"=== Facial Motion 재접속 완료 ({reconnectedCount}/{facialMotionClients.Count}개) ==="); + } + else + { + LogError("재접속에 성공한 클라이언트가 없습니다!"); + } + } + + #endregion + + #region 스크린샷 기능 + + /// + /// 일반 스크린샷 촬영 + /// + public void CaptureScreenshot() + { + if (screenshotCamera == null) + { + LogError("촬영할 카메라가 설정되지 않았습니다!"); + return; + } + + string fileName = GenerateFileName("png"); + string fullPath = Path.Combine(screenshotSavePath, fileName); + + try + { + // 렌더 텍스처 생성 + RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24); + RenderTexture currentRT = screenshotCamera.targetTexture; + + // 카메라로 렌더링 + screenshotCamera.targetTexture = rt; + screenshotCamera.Render(); + + // 텍스처를 Texture2D로 변환 + RenderTexture.active = rt; + Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGB24, false); + screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0); + screenshot.Apply(); + + // PNG로 저장 + byte[] bytes = screenshot.EncodeToPNG(); + File.WriteAllBytes(fullPath, bytes); + + // 정리 + screenshotCamera.targetTexture = currentRT; + RenderTexture.active = null; + Destroy(rt); + Destroy(screenshot); + + Log($"스크린샷 저장 완료: {fullPath}"); + } + catch (Exception e) + { + LogError($"스크린샷 촬영 실패: {e.Message}"); + } + } + + /// + /// 알파 채널 포함 스크린샷 촬영 + /// NiloToon Prepass 버퍼의 G 채널을 알파로 사용 + /// + public void CaptureAlphaScreenshot() + { + if (screenshotCamera == null) + { + LogError("촬영할 카메라가 설정되지 않았습니다!"); + return; + } + + if (alphaShader == null) + { + LogError("알파 셰이더가 설정되지 않았습니다!"); + return; + } + + string fileName = GenerateFileName("png", "_Alpha"); + string fullPath = Path.Combine(screenshotSavePath, fileName); + + try + { + // 렌더 텍스처 생성 + RenderTexture rt = new RenderTexture(screenshotWidth, screenshotHeight, 24); + RenderTexture currentRT = screenshotCamera.targetTexture; + + // 카메라로 렌더링 + screenshotCamera.targetTexture = rt; + screenshotCamera.Render(); + + // NiloToon Prepass 버퍼 가져오기 + Texture niloToonPrepassBuffer = Shader.GetGlobalTexture(niloToonPrepassBufferName); + + if (niloToonPrepassBuffer == null) + { + LogError($"NiloToon Prepass 버퍼를 찾을 수 없습니다: {niloToonPrepassBufferName}"); + screenshotCamera.targetTexture = currentRT; + Destroy(rt); + return; + } + + // 알파 합성용 머티리얼 생성 + if (alphaMaterial == null) + { + alphaMaterial = new Material(alphaShader); + } + + // 알파 채널 합성 + RenderTexture alphaRT = new RenderTexture(screenshotWidth, screenshotHeight, 0, RenderTextureFormat.ARGB32); + alphaMaterial.SetTexture("_MainTex", rt); + alphaMaterial.SetTexture("_AlphaTex", niloToonPrepassBuffer); + alphaMaterial.SetFloat("_BlurRadius", alphaBlurRadius); + + // Blit으로 알파 합성 + Graphics.Blit(rt, alphaRT, alphaMaterial); + + // 텍스처를 Texture2D로 변환 + RenderTexture.active = alphaRT; + Texture2D screenshot = new Texture2D(screenshotWidth, screenshotHeight, TextureFormat.RGBA32, false); + screenshot.ReadPixels(new Rect(0, 0, screenshotWidth, screenshotHeight), 0, 0); + screenshot.Apply(); + + // PNG로 저장 + byte[] bytes = screenshot.EncodeToPNG(); + File.WriteAllBytes(fullPath, bytes); + + // 정리 + screenshotCamera.targetTexture = currentRT; + RenderTexture.active = null; + Destroy(rt); + Destroy(alphaRT); + Destroy(screenshot); + + Log($"알파 스크린샷 저장 완료: {fullPath}"); + } + catch (Exception e) + { + LogError($"알파 스크린샷 촬영 실패: {e.Message}"); + } + } + + /// + /// 스크린샷 저장 폴더 열기 + /// + public void OpenScreenshotFolder() + { + if (Directory.Exists(screenshotSavePath)) + { + System.Diagnostics.Process.Start(screenshotSavePath); + Log($"저장 폴더 열기: {screenshotSavePath}"); + } + else + { + LogError($"저장 폴더가 존재하지 않습니다: {screenshotSavePath}"); + } + } + + /// + /// 파일명 생성 + /// + private string GenerateFileName(string extension, string suffix = "") + { + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + return $"{screenshotFilePrefix}{suffix}_{timestamp}.{extension}"; + } + + #endregion + + #region 모션 녹화 기능 (EasyMotion Recorder + OptiTrack Motive) + + /// + /// 모션 녹화 시작 (EasyMotion Recorder + OptiTrack Motive) + /// + public void StartMotionRecording() + { + // OptiTrack 녹화 시작 (옵션이 켜져 있을 때만) + bool optitrackStarted = false; + if (recordOptiTrackWithMotion) + { + if (optitrackClient != null) + { + try + { + optitrackStarted = optitrackClient.StartRecording(); + if (optitrackStarted) + { + Log("OptiTrack Motive 녹화 시작 성공"); + } + else + { + LogError("OptiTrack Motive 녹화 시작 실패"); + } + } + catch (System.Exception e) + { + LogError($"OptiTrack 녹화 시작 오류: {e.Message}"); + } + } + else + { + Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 건너뜀"); + } + } + else + { + Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 건너뜀"); + } + + // EasyMotion Recorder 녹화 시작 + int startedCount = 0; + if (motionRecorders != null && motionRecorders.Count > 0) + { + foreach (var recorder in motionRecorders) + { + if (recorder != null) + { + try + { + // RecordStart 메서드 호출 + var method = recorder.GetType().GetMethod("RecordStart"); + if (method != null) + { + method.Invoke(recorder, null); + startedCount++; + } + } + catch (System.Exception e) + { + LogError($"레코더 시작 실패 ({recorder.name}): {e.Message}"); + } + } + } + + if (startedCount > 0) + { + Log($"EasyMotion 녹화 시작 ({startedCount}/{motionRecorders.Count}개 레코더)"); + } + else + { + LogError("녹화를 시작한 EasyMotion 레코더가 없습니다!"); + } + } + else + { + Log("EasyMotion Recorder 없음 - EasyMotion 녹화 건너뜀"); + } + + // 하나라도 성공하면 녹화 중 상태로 설정 + if (optitrackStarted || startedCount > 0) + { + isRecording = true; + Log($"=== 녹화 시작 완료 ==="); + if (recordOptiTrackWithMotion) + { + Log($"OptiTrack: {(optitrackStarted ? "시작됨" : "시작 안됨")}"); + } + else + { + Log($"OptiTrack: 옵션 꺼짐"); + } + Log($"EasyMotion: {startedCount}개 레코더 시작됨"); + } + else + { + LogError("녹화를 시작할 수 있는 시스템이 없습니다!"); + } + } + + /// + /// 모션 녹화 중지 (EasyMotion Recorder + OptiTrack Motive) + /// + public void StopMotionRecording() + { + // OptiTrack 녹화 중지 (옵션이 켜져 있을 때만) + bool optitrackStopped = false; + if (recordOptiTrackWithMotion) + { + if (optitrackClient != null) + { + try + { + optitrackStopped = optitrackClient.StopRecording(); + if (optitrackStopped) + { + Log("OptiTrack Motive 녹화 중지 성공"); + } + else + { + LogError("OptiTrack Motive 녹화 중지 실패"); + } + } + catch (System.Exception e) + { + LogError($"OptiTrack 녹화 중지 오류: {e.Message}"); + } + } + else + { + Log("OptiTrack 클라이언트 없음 - OptiTrack 녹화 중지 건너뜀"); + } + } + else + { + Log("OptiTrack 녹화 옵션 꺼짐 - OptiTrack 녹화 중지 건너뜀"); + } + + // EasyMotion Recorder 녹화 중지 + int stoppedCount = 0; + if (motionRecorders != null && motionRecorders.Count > 0) + { + foreach (var recorder in motionRecorders) + { + if (recorder != null) + { + try + { + // RecordEnd 메서드 호출 + var method = recorder.GetType().GetMethod("RecordEnd"); + if (method != null) + { + method.Invoke(recorder, null); + stoppedCount++; + } + } + catch (System.Exception e) + { + LogError($"레코더 중지 실패 ({recorder.name}): {e.Message}"); + } + } + } + + if (stoppedCount > 0) + { + Log($"EasyMotion 녹화 중지 ({stoppedCount}/{motionRecorders.Count}개 레코더)"); + } + else + { + LogError("녹화를 중지한 EasyMotion 레코더가 없습니다!"); + } + } + else + { + Log("EasyMotion Recorder 없음 - EasyMotion 녹화 중지 건너뜀"); + } + + // 하나라도 성공하면 녹화 중지 상태로 설정 + if (optitrackStopped || stoppedCount > 0) + { + isRecording = false; + Log($"=== 녹화 중지 완료 ==="); + Log($"OptiTrack: {(optitrackStopped ? "중지됨" : "중지 안됨")}"); + Log($"EasyMotion: {stoppedCount}개 레코더 중지됨"); + } + else + { + LogError("녹화를 중지할 수 있는 시스템이 없습니다!"); + } + } + + /// + /// 모션 녹화 토글 (시작/중지) + /// + public void ToggleMotionRecording() + { + if (isRecording) + { + StopMotionRecording(); + } + else + { + StartMotionRecording(); + } + } + + /// + /// 현재 녹화 중인지 여부 반환 + /// + public bool IsRecording() + { + return isRecording; + } + + #endregion + + /// + /// 명령어 실행 - WebSocket에서 받은 명령을 처리 + /// + public void ExecuteCommand(string command, Dictionary parameters) + { + Log($"명령어 실행: {command}"); + + switch (command) + { + // OptiTrack 마커 + case "toggle_optitrack_markers": + ToggleOptitrackMarkers(); + break; + + case "show_optitrack_markers": + ShowOptitrackMarkers(); + break; + + case "hide_optitrack_markers": + HideOptitrackMarkers(); + break; + + // OptiTrack 재접속 + case "reconnect_optitrack": + ReconnectOptitrack(); + break; + + // Facial Motion 재접속 + case "reconnect_facial_motion": + ReconnectFacialMotion(); + break; + + case "refresh_facial_motion_clients": + RefreshFacialMotionClients(); + break; + + // EasyMotion Recorder + case "start_motion_recording": + StartMotionRecording(); + break; + + case "stop_motion_recording": + StopMotionRecording(); + break; + + case "toggle_motion_recording": + ToggleMotionRecording(); + break; + + case "refresh_motion_recorders": + RefreshMotionRecorders(); + break; + + // 스크린샷 + case "capture_screenshot": + CaptureScreenshot(); + break; + + case "capture_alpha_screenshot": + CaptureAlphaScreenshot(); + break; + + case "open_screenshot_folder": + OpenScreenshotFolder(); + break; + + default: + LogError($"알 수 없는 명령어: {command}"); + break; + } + } + + private void OnDestroy() + { + if (alphaMaterial != null) + { + Destroy(alphaMaterial); + } + } + + private void Log(string message) + { + if (enableDebugLog) + { + Debug.Log($"[SystemController] {message}"); + } + } + + private void LogError(string message) + { + Debug.LogError($"[SystemController] {message}"); + } +} diff --git a/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs.meta b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs.meta new file mode 100644 index 00000000..ebd35130 --- /dev/null +++ b/Assets/Scripts/Streamingle/StreamingleControl/Controllers/SystemController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 7f8e9a1b2c3d4e5f6a7b8c9d0e1f2a3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Streamdeck/DEBUG_MARKER_BUTTON.md b/Streamdeck/DEBUG_MARKER_BUTTON.md new file mode 100644 index 00000000..fb9c4d8c --- /dev/null +++ b/Streamdeck/DEBUG_MARKER_BUTTON.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e38c2d06feb3eee2ec4a170d5f5aba9de1169b140aab057baa1793c25e12c17 +size 7231 diff --git a/Streamdeck/DEPLOY_README.md b/Streamdeck/DEPLOY_README.md new file mode 100644 index 00000000..556fdbee --- /dev/null +++ b/Streamdeck/DEPLOY_README.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e1884fb316061cb5cfa2b379a2fa0e9b4755cf71274d0ebe40ff2dbc4fc6bf09 +size 3945 diff --git a/Streamdeck/HOW_TO_DEBUG_STREAMDOCK.md b/Streamdeck/HOW_TO_DEBUG_STREAMDOCK.md new file mode 100644 index 00000000..ff3ecd42 --- /dev/null +++ b/Streamdeck/HOW_TO_DEBUG_STREAMDOCK.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14e4ff00a3203c3e6dc339d08f0f41a13d735de242dd5658c39f387bb8c74dc6 +size 7149 diff --git a/Streamdeck/STREAMDOCK_SDK_REFERENCE.md b/Streamdeck/STREAMDOCK_SDK_REFERENCE.md new file mode 100644 index 00000000..cd051eb5 --- /dev/null +++ b/Streamdeck/STREAMDOCK_SDK_REFERENCE.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a5038cda15b8e42ca983d9b93364ea5c2eb96d3ac2b122ad303f05d69292651 +size 19329 diff --git a/Streamdeck/SYSTEM_CONTROLLER_GUIDE.md b/Streamdeck/SYSTEM_CONTROLLER_GUIDE.md new file mode 100644 index 00000000..861b81ab --- /dev/null +++ b/Streamdeck/SYSTEM_CONTROLLER_GUIDE.md @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b87c6a3ef7f826ade6cffd8d2abbffa0757831d9afd12a46b231e2ffbdf0044 +size 12152 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/facial_motion_reconnect_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/facial_motion_reconnect_icon.png new file mode 100644 index 00000000..b92e996e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/facial_motion_reconnect_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:06aa5252be7be66b2078f37f8d7338ed504240745cd1708038e2f16fc4f0f61a +size 20476 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon.png new file mode 100644 index 00000000..2a9dad9e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f238d20b3f67c8bd416cef5c31e7fe28c0430c7df9aef6ae213d7b3e4606bc8e +size 16518 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon_off.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon_off.png new file mode 100644 index 00000000..5521cf76 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/motion_record_icon_off.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:783bc91d3d4e48fa184fcbf474b21f3f89b922649723589189f2f514c779d526 +size 74030 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_group_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_group_icon.png new file mode 100644 index 00000000..ac62b10b --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_group_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:38e09c258916f079198fc6efd17353dd1bab563734ef55f03703485c091c11e1 +size 33589 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon.png new file mode 100644 index 00000000..549b28c0 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d081e41039c4788f35acf4504095668c18494c2ca694ccc20c66f42f49907587 +size 11770 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon_off.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon_off.png new file mode 100644 index 00000000..19b2665e --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_marker_icon_off.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04e1e9a6ad21d2895cb47d0d709a6ada0bd26ef7d34013f618d7555b9fc7d50c +size 8427 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_reconnect_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_reconnect_icon.png new file mode 100644 index 00000000..c8bbdd99 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/optitrack_reconnect_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9f34a64a3824abbe6077f63e42312662cfb1f919db5a9fbef815286b7d2e2a64 +size 12469 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_alpha_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_alpha_icon.png new file mode 100644 index 00000000..5d62f435 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_alpha_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f384bc263a6b98665b57ace0491acc5be88601905cad90fd5ba49812f04d863 +size 13506 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_icon.png b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_icon.png new file mode 100644 index 00000000..fca6e224 --- /dev/null +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/images/screenshot_icon.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f67e498ca058a9f3d5a3e3c7a96a56c3471154da058b52ce376be9976d3fb9b +size 13989 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json index 6520d892..7896416a 100644 --- a/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/manifest.json @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf09907ab34e2b80144ee1883f2db40c7c622dcdf7b8542d252f4a0f4bac452c -size 3982 +oid sha256:12627ac0969f34220ce859abbc9ace8b19b657b48b7c923575b3d1a30686fa66 +size 7771 diff --git a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js index 288aef3a..878dfd6d 100644 --- a/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js +++ b/Streamdeck/com.mirabox.streamingle.sdPlugin/plugin/index.js @@ -69,36 +69,199 @@ function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inAction case 'didReceiveSettings': if (jsonObj.payload && jsonObj.context) { const newSettings = jsonObj.payload.settings || {}; + + // actionType이 없으면 action UUID로 판단해서 강제 설정 + if (!newSettings.actionType && jsonObj.action) { + console.log('⚠️ actionType 없음! action UUID로 재설정:', jsonObj.action); + if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') { + newSettings.actionType = 'optitrack_marker_toggle'; + console.log('✅ 마커 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') { + newSettings.actionType = 'motion_recording_toggle'; + console.log('🎬 모션 녹화 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') { + newSettings.actionType = 'optitrack_reconnect'; + console.log('🔄 OptiTrack 재접속 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') { + newSettings.actionType = 'facial_motion_reconnect'; + console.log('😊 Facial Motion 재접속 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') { + newSettings.actionType = 'screenshot'; + console.log('📸 스크린샷 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') { + newSettings.actionType = 'screenshot_alpha'; + console.log('📷 알파 스크린샷 버튼으로 강제 설정됨'); + + // StreamDock SDK에 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: newSettings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 강제 설정 저장:', newSettings); + } + } else if (jsonObj.action === 'com.mirabox.streamingle.item') { + newSettings.actionType = 'item'; + } else if (jsonObj.action === 'com.mirabox.streamingle.event') { + newSettings.actionType = 'event'; + } else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') { + newSettings.actionType = 'avatar_outfit'; + } else { + newSettings.actionType = 'camera'; + } + } + buttonContexts.set(jsonObj.context, newSettings); console.log('⚙️ 설정 업데이트:', newSettings); updateButtonTitle(jsonObj.context); } break; case 'willAppear': - console.log('👀 버튼 나타남:', jsonObj.context); + console.log('============================================'); + console.log('👀 버튼 나타남 이벤트'); + console.log('🔍 Context:', jsonObj.context); console.log('🔍 Action UUID:', jsonObj.action); - + let settings = jsonObj.payload?.settings || {}; - console.log('⚙️ 초기 설정:', settings); - + console.log('⚙️ 초기 설정:', JSON.stringify(settings)); + // action UUID로 actionType 결정 if (jsonObj.action === 'com.mirabox.streamingle.item') { settings.actionType = 'item'; - console.log('🎯 아이템 컨트롤러 등록:', jsonObj.context); + console.log('🎯 아이템 컨트롤러 등록'); } else if (jsonObj.action === 'com.mirabox.streamingle.event') { settings.actionType = 'event'; - console.log('🎯 이벤트 컨트롤러 등록:', jsonObj.context); + console.log('🎯 이벤트 컨트롤러 등록'); } else if (jsonObj.action === 'com.mirabox.streamingle.avatar_outfit') { settings.actionType = 'avatar_outfit'; - console.log('👗 아바타 의상 컨트롤러 등록:', jsonObj.context); + console.log('👗 아바타 의상 컨트롤러 등록'); + } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_marker_toggle') { + settings.actionType = 'optitrack_marker_toggle'; + console.log('✅✅✅ MARKER BUTTON DETECTED ✅✅✅'); + console.log('✅ This is the OptiTrack Marker Toggle button!'); + console.log('✅ When clicked, it should send: toggle_optitrack_markers'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '마커\nON'); + } else if (jsonObj.action === 'com.mirabox.streamingle.motion_recording_toggle') { + settings.actionType = 'motion_recording_toggle'; + console.log('🎬🎬🎬 MOTION RECORDING BUTTON DETECTED 🎬🎬🎬'); + console.log('🎬 This is the Motion Recording Toggle button!'); + console.log('🎬 When clicked, it should send: toggle_motion_recording'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '녹화\nOFF'); + } else if (jsonObj.action === 'com.mirabox.streamingle.optitrack_reconnect') { + settings.actionType = 'optitrack_reconnect'; + console.log('🔄🔄🔄 OPTITRACK RECONNECT BUTTON DETECTED 🔄🔄🔄'); + console.log('🔄 This is the OptiTrack Reconnect button!'); + console.log('🔄 When clicked, it should send: reconnect_optitrack'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '옵티트랙\n재접속'); + } else if (jsonObj.action === 'com.mirabox.streamingle.facial_motion_reconnect') { + settings.actionType = 'facial_motion_reconnect'; + console.log('😊😊😊 FACIAL MOTION RECONNECT BUTTON DETECTED 😊😊😊'); + console.log('😊 This is the Facial Motion Reconnect button!'); + console.log('😊 When clicked, it should send: reconnect_facial_motion'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '페이셜\n재접속'); + } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot') { + settings.actionType = 'screenshot'; + console.log('📸📸📸 SCREENSHOT BUTTON DETECTED 📸📸📸'); + console.log('📸 This is the Screenshot button!'); + console.log('📸 When clicked, it should send: capture_screenshot'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '스크린샷'); + } else if (jsonObj.action === 'com.mirabox.streamingle.screenshot_alpha') { + settings.actionType = 'screenshot_alpha'; + console.log('📷📷📷 ALPHA SCREENSHOT BUTTON DETECTED 📷📷📷'); + console.log('📷 This is the Alpha Screenshot button!'); + console.log('📷 When clicked, it should send: capture_alpha_screenshot'); + + // 기본 제목 설정 + setButtonTitle(jsonObj.context, '알파\n스크린샷'); } else { settings.actionType = 'camera'; - console.log('📹 카메라 컨트롤러 등록:', jsonObj.context); + console.log('📹 카메라 컨트롤러 등록 (기본값)'); + console.log('📹 This button will send camera switch messages'); } - + console.log('🎯 최종 actionType:', settings.actionType); buttonContexts.set(jsonObj.context, settings); - console.log('💾 설정 저장됨:', settings); + + // StreamDock SDK에도 설정 저장 + if (websocket) { + const setSettingsMessage = { + event: 'setSettings', + context: jsonObj.context, + payload: settings + }; + websocket.send(JSON.stringify(setSettingsMessage)); + console.log('💾 StreamDock SDK에 설정 저장:', settings); + } + + console.log('💾 설정 저장 완료'); + console.log('============================================'); updateButtonTitle(jsonObj.context); // Unity가 이미 연결되어 있다면 Property Inspector에 상태 전송 @@ -140,7 +303,8 @@ function connectElgatoStreamDeckSocket(inPort, inUUID, inEvent, inInfo, inAction case 'keyUp': console.log('🔘 버튼 클릭됨!'); - handleButtonClick(jsonObj.context); + console.log('🔍 Action UUID:', jsonObj.action); + handleButtonClick(jsonObj.context, jsonObj.action); break; case 'sendToPlugin': @@ -277,47 +441,130 @@ function connectToUnity() { } // 버튼 클릭 처리 -function handleButtonClick(context) { +function handleButtonClick(context, actionUUID) { console.log('🎯 버튼 클릭 처리 시작'); console.log('📍 컨텍스트:', context); + console.log('📍 Action UUID:', actionUUID); console.log('🔌 Unity 연결 상태:', isUnityConnected); - + if (!isUnityConnected || !unitySocket) { console.log('🔄 Unity 연결되지 않음, 재연결 시도...'); connectToUnity(); // 연결 후 잠시 대기한 후 다시 시도 setTimeout(() => { if (isUnityConnected && unitySocket) { - handleButtonClick(context); + handleButtonClick(context, actionUUID); } else { console.error('❌ Unity 재연결 실패'); } }, 1000); return; } - + // context별 settings 사용 const settings = getCurrentSettings(context); - const actionType = settings.actionType || 'camera'; // 기본값은 camera - - console.log('🎯 액션 타입:', actionType); - + let actionType = settings.actionType; + + // actionType이 없으면 action UUID로 판단 + if (!actionType && actionUUID) { + console.log('⚠️ actionType 없음! Action UUID로 결정:', actionUUID); + if (actionUUID === 'com.mirabox.streamingle.optitrack_marker_toggle') { + actionType = 'optitrack_marker_toggle'; + console.log('✅ 마커 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.motion_recording_toggle') { + actionType = 'motion_recording_toggle'; + console.log('🎬 모션 녹화 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.optitrack_reconnect') { + actionType = 'optitrack_reconnect'; + console.log('🔄 OptiTrack 재접속 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.facial_motion_reconnect') { + actionType = 'facial_motion_reconnect'; + console.log('😊 Facial Motion 재접속 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.screenshot') { + actionType = 'screenshot'; + console.log('📸 스크린샷 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.screenshot_alpha') { + actionType = 'screenshot_alpha'; + console.log('📷 알파 스크린샷 버튼으로 인식'); + } else if (actionUUID === 'com.mirabox.streamingle.item') { + actionType = 'item'; + } else if (actionUUID === 'com.mirabox.streamingle.event') { + actionType = 'event'; + } else if (actionUUID === 'com.mirabox.streamingle.avatar_outfit') { + actionType = 'avatar_outfit'; + } else { + actionType = 'camera'; + } + + // 설정에 저장 + settings.actionType = actionType; + setCurrentSettings(context, settings); + } + + if (!actionType) { + actionType = 'camera'; // 최종 폴백 + } + + console.log('╔════════════════════════════════════════╗'); + console.log('║ BUTTON CLICKED - DEBUG INFO ║'); + console.log('╠════════════════════════════════════════╣'); + console.log(' Context:', context); + console.log(' Action UUID:', actionUUID); + console.log(' ActionType:', actionType); + console.log(' All Settings:', JSON.stringify(settings)); + console.log('╚════════════════════════════════════════╝'); + switch (actionType) { case 'camera': + console.log('➡️ Routing to: CAMERA handler'); + console.log(' Will send: {"type":"switch_camera","data":{...}}'); handleCameraAction(settings); break; case 'item': + console.log('➡️ Routing to: ITEM handler'); handleItemAction(settings); break; case 'event': + console.log('➡️ Routing to: EVENT handler'); handleEventAction(settings); break; case 'avatar_outfit': + console.log('➡️ Routing to: AVATAR OUTFIT handler'); handleAvatarOutfitAction(settings); break; + case 'optitrack_marker_toggle': + console.log('➡️ Routing to: MARKER TOGGLE handler'); + console.log(' Will send: {"type":"toggle_optitrack_markers"}'); + handleOptitrackMarkerToggle(context); + break; + case 'motion_recording_toggle': + console.log('➡️ Routing to: MOTION RECORDING handler'); + console.log(' Will send: {"type":"toggle_motion_recording"}'); + handleMotionRecordingToggle(context); + break; + case 'optitrack_reconnect': + console.log('➡️ Routing to: OPTITRACK RECONNECT handler'); + console.log(' Will send: {"type":"reconnect_optitrack"}'); + handleOptitrackReconnect(context); + break; + case 'facial_motion_reconnect': + console.log('➡️ Routing to: FACIAL MOTION RECONNECT handler'); + console.log(' Will send: {"type":"reconnect_facial_motion"}'); + handleFacialMotionReconnect(context); + break; + case 'screenshot': + console.log('➡️ Routing to: SCREENSHOT handler'); + console.log(' Will send: {"type":"capture_screenshot"}'); + handleScreenshot(context); + break; + case 'screenshot_alpha': + console.log('➡️ Routing to: ALPHA SCREENSHOT handler'); + console.log(' Will send: {"type":"capture_alpha_screenshot"}'); + handleAlphaScreenshot(context); + break; default: - console.log('⚠️ 알 수 없는 액션 타입:', actionType); - // 기본적으로 카메라 액션으로 처리 + console.log('⚠️ WARNING: Unknown actionType:', actionType); + console.log(' Defaulting to CAMERA handler'); handleCameraAction(settings); } } @@ -477,6 +724,244 @@ function handleAvatarOutfitAction(settings) { } } +// OptiTrack 마커 토글 액션 처리 +function handleOptitrackMarkerToggle(context) { + console.log('🎯 OptiTrack 마커 토글 실행'); + + // Unity에 마커 토글 요청 + const message = JSON.stringify({ + type: 'toggle_optitrack_markers' + }); + + console.log('📤 Unity에 OptiTrack 마커 토글 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 버튼 상태 토글 (0 <-> 1) + toggleButtonState(context); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + +// 버튼 상태 토글 함수 +function toggleButtonState(context) { + const settings = getCurrentSettings(context); + const currentState = settings.currentState || 0; + const newState = currentState === 0 ? 1 : 0; + + // 설정 업데이트 + settings.currentState = newState; + setCurrentSettings(context, settings); + + // 버튼 상태 변경 + const stateMessage = { + event: 'setState', + context: context, + payload: { + state: newState, + target: 0 // hardware and software + } + }; + + if (websocket) { + websocket.send(JSON.stringify(stateMessage)); + console.log('🎨 버튼 상태 업데이트:', newState === 0 ? 'ON' : 'OFF'); + } + + // 제목도 함께 변경 + const title = newState === 0 ? '마커\nON' : '마커\nOFF'; + setButtonTitle(context, title); +} + +// 모션 녹화 토글 액션 처리 +function handleMotionRecordingToggle(context) { + console.log('🎬 모션 녹화 토글 실행'); + + // Unity에 모션 녹화 토글 요청 + const message = JSON.stringify({ + type: 'toggle_motion_recording' + }); + + console.log('📤 Unity에 모션 녹화 토글 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 버튼 상태 토글 (0 <-> 1) + toggleMotionRecordingButtonState(context); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + +// 모션 녹화 버튼 상태 토글 함수 +function toggleMotionRecordingButtonState(context) { + const settings = getCurrentSettings(context); + const currentState = settings.currentState || 0; + const newState = currentState === 0 ? 1 : 0; + + // 설정 업데이트 + settings.currentState = newState; + setCurrentSettings(context, settings); + + // 버튼 상태 변경 + const stateMessage = { + event: 'setState', + context: context, + payload: { + state: newState, + target: 0 // hardware and software + } + }; + + if (websocket) { + websocket.send(JSON.stringify(stateMessage)); + console.log('🎨 녹화 버튼 상태 업데이트:', newState === 0 ? 'OFF' : 'REC'); + } + + // 제목도 함께 변경 + const title = newState === 0 ? '녹화\nOFF' : '녹화\nREC'; + setButtonTitle(context, title); +} + +// OptiTrack 재접속 액션 처리 +function handleOptitrackReconnect(context) { + console.log('🔄 OptiTrack 재접속 실행'); + + // Unity에 OptiTrack 재접속 요청 + const message = JSON.stringify({ + type: 'reconnect_optitrack' + }); + + console.log('📤 Unity에 OptiTrack 재접속 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음 + // 피드백을 위해 제목을 잠시 변경할 수 있음 + setButtonTitle(context, '재접속\n중...'); + + // 1초 후 원래 제목으로 복구 + setTimeout(() => { + setButtonTitle(context, '옵티트랙\n재접속'); + }, 1000); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + +// Facial Motion 재접속 액션 처리 +function handleFacialMotionReconnect(context) { + console.log('😊 Facial Motion 재접속 실행'); + + // Unity에 Facial Motion 재접속 요청 + const message = JSON.stringify({ + type: 'reconnect_facial_motion' + }); + + console.log('📤 Unity에 Facial Motion 재접속 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 재접속은 상태가 없는 단순 액션이므로 버튼 상태 변경 없음 + // 피드백을 위해 제목을 잠시 변경할 수 있음 + setButtonTitle(context, '재접속\n중...'); + + // 1초 후 원래 제목으로 복구 + setTimeout(() => { + setButtonTitle(context, '페이셜\n재접속'); + }, 1000); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + +// 스크린샷 액션 처리 +function handleScreenshot(context) { + console.log('📸 스크린샷 촬영 실행'); + + // Unity에 스크린샷 요청 + const message = JSON.stringify({ + type: 'capture_screenshot' + }); + + console.log('📤 Unity에 스크린샷 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 피드백을 위해 제목을 잠시 변경 + setButtonTitle(context, '촬영\n중...'); + + // 1초 후 원래 제목으로 복구 + setTimeout(() => { + setButtonTitle(context, '스크린샷'); + }, 1000); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + +// 알파 스크린샷 액션 처리 +function handleAlphaScreenshot(context) { + console.log('📷 알파 스크린샷 촬영 실행'); + + // Unity에 알파 스크린샷 요청 + const message = JSON.stringify({ + type: 'capture_alpha_screenshot' + }); + + console.log('📤 Unity에 알파 스크린샷 요청 전송:', message); + console.log('🔍 Unity 연결 상태:', isUnityConnected); + console.log('🔍 Unity 소켓 상태:', !!unitySocket); + + if (unitySocket && unitySocket.readyState === WebSocket.OPEN) { + unitySocket.send(message); + console.log('✅ 메시지 전송 완료'); + + // 피드백을 위해 제목을 잠시 변경 + setButtonTitle(context, '촬영\n중...'); + + // 1초 후 원래 제목으로 복구 + setTimeout(() => { + setButtonTitle(context, '알파\n스크린샷'); + }, 1000); + } else { + console.error('❌ Unity 소켓이 연결되지 않음'); + console.log('🔄 Unity 재연결 시도...'); + connectToUnity(); + } +} + // Property Inspector 메시지 처리 function handlePropertyInspectorMessage(payload, context, actionUUID) { const command = payload.command; @@ -1243,6 +1728,28 @@ function handleUnityMessage(data) { } } +// 버튼 제목을 직접 설정하는 함수 +function setButtonTitle(context, title) { + if (!websocket || !context) { + console.log('🚫 WebSocket 또는 context 없음 - 제목 설정 건너뜀'); + return; + } + + console.log(`🏷️ 버튼 제목 설정: "${title}" (Context: ${context})`); + + const message = { + event: 'setTitle', + context: context, + payload: { + title: title, + target: 0 // hardware and software + } + }; + + websocket.send(JSON.stringify(message)); + console.log('✅ setTitle 메시지 전송 완료'); +} + // 모든 버튼의 제목 업데이트 function updateAllButtonTitles() { for (const context of buttonContexts.keys()) { @@ -1408,8 +1915,40 @@ function updateButtonTitle(context) { console.log('👗 아바타 목록이 없거나 인덱스가 범위를 벗어남'); console.log('👗 목록 길이:', avatarOutfitList ? avatarOutfitList.length : 'null'); } + } else if (actionType === 'optitrack_marker_toggle') { + // OptiTrack 마커 토글 버튼 + const currentState = settings.currentState || 0; + title = currentState === 0 ? '마커\nON' : '마커\nOFF'; + isActive = true; // 항상 활성 상태 + console.log('🎯 OptiTrack 마커 버튼 제목:', title, '(State:', currentState, ')'); + } else if (actionType === 'motion_recording_toggle') { + // 모션 녹화 토글 버튼 + const currentState = settings.currentState || 0; + title = currentState === 0 ? '녹화\nOFF' : '녹화\nREC'; + isActive = true; // 항상 활성 상태 + console.log('🎬 모션 녹화 버튼 제목:', title, '(State:', currentState, ')'); + } else if (actionType === 'optitrack_reconnect') { + // OptiTrack 재접속 버튼 + title = '옵티트랙\n재접속'; + isActive = true; // 항상 활성 상태 + console.log('🔄 OptiTrack 재접속 버튼 제목:', title); + } else if (actionType === 'facial_motion_reconnect') { + // Facial Motion 재접속 버튼 + title = '페이셜\n재접속'; + isActive = true; // 항상 활성 상태 + console.log('😊 Facial Motion 재접속 버튼 제목:', title); + } else if (actionType === 'screenshot') { + // 스크린샷 버튼 + title = '스크린샷'; + isActive = true; // 항상 활성 상태 + console.log('📸 스크린샷 버튼 제목:', title); + } else if (actionType === 'screenshot_alpha') { + // 알파 스크린샷 버튼 + title = '알파\n스크린샷'; + isActive = true; // 항상 활성 상태 + console.log('📷 알파 스크린샷 버튼 제목:', title); } - + // StreamDock에 제목 업데이트 요청 const message = { event: 'setTitle', diff --git a/Streamdeck/deploy-plugin.bat b/Streamdeck/deploy-plugin.bat new file mode 100644 index 00000000..b0de686c --- /dev/null +++ b/Streamdeck/deploy-plugin.bat @@ -0,0 +1,39 @@ +@echo off +chcp 65001 >nul + +:: Check for admin rights +net session >nul 2>&1 +if %errorLevel% == 0 ( + goto :run_script +) else ( + echo Requesting administrator privileges... + echo. + + :: Restart as administrator + powershell -Command "Start-Process '%~f0' -Verb RunAs" + exit /b +) + +:run_script +echo. +echo ======================================== +echo StreamDeck Plugin Auto Deploy +echo ======================================== +echo. +echo Running PowerShell deployment script... +echo. + +REM Run PowerShell script with admin rights +powershell -NoProfile -ExecutionPolicy Bypass -Command "& '%~dp0deploy-plugin.ps1'" + +if %ERRORLEVEL% NEQ 0 ( + echo. + echo ERROR: Deployment failed! + echo. + pause + exit /b 1 +) + +echo. +echo Complete! Window will close in 2 seconds... +timeout /t 2 /nobreak >nul diff --git a/Streamdeck/deploy-plugin.ps1 b/Streamdeck/deploy-plugin.ps1 new file mode 100644 index 00000000..9d2d4953 --- /dev/null +++ b/Streamdeck/deploy-plugin.ps1 @@ -0,0 +1,166 @@ +# StreamDeck Plugin Auto Deployment Script +# Usage: Run .\deploy-plugin.ps1 in PowerShell + +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " StreamDeck Plugin Auto Deploy" -ForegroundColor Cyan +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" + +# Path settings +$PluginSourcePath = "$PSScriptRoot\com.mirabox.streamingle.sdPlugin" +$PluginDestPath = "$env:APPDATA\Hotspot\StreamDock\plugins\com.mirabox.streamingle.sdPlugin" +$StreamDeckExe = "C:\Program Files\Hotspot\StreamDock\StreamDock.exe" + +# Check StreamDock executable path +$StreamDeckExeFound = $false +if (-not (Test-Path $StreamDeckExe)) { + Write-Host "WARNING: StreamDock executable not found: $StreamDeckExe" -ForegroundColor Yellow + Write-Host "Checking alternative paths..." -ForegroundColor Yellow + + # Check alternative paths + $AlternativePaths = @( + "C:\Program Files (x86)\Hotspot\StreamDock\StreamDock.exe", + "C:\Program Files\Elgato\StreamDeck\StreamDeck.exe", + "$env:ProgramFiles\Hotspot\StreamDock\StreamDock.exe", + "${env:ProgramFiles(x86)}\Hotspot\StreamDock\StreamDock.exe", + "$env:LOCALAPPDATA\Hotspot\StreamDock\StreamDock.exe", + "$env:APPDATA\Hotspot\StreamDock\StreamDock.exe" + ) + + foreach ($path in $AlternativePaths) { + if (Test-Path $path) { + $StreamDeckExe = $path + $StreamDeckExeFound = $true + Write-Host "SUCCESS: StreamDock executable found: $StreamDeckExe" -ForegroundColor Green + break + } + } + + if (-not $StreamDeckExeFound) { + Write-Host "WARNING: StreamDock executable not found!" -ForegroundColor Yellow + Write-Host "Plugin files will be copied. Please restart StreamDock manually." -ForegroundColor Yellow + } +} else { + $StreamDeckExeFound = $true +} + +# Step 1: Stop StreamDock process +Write-Host "Step 1: Stopping StreamDock process..." -ForegroundColor Yellow +$StreamDeckProcess = Get-Process -Name "StreamDock" -ErrorAction SilentlyContinue + +if ($StreamDeckProcess) { + Write-Host " StreamDock process found (PID: $($StreamDeckProcess.Id))" -ForegroundColor Gray + + try { + Stop-Process -Name "StreamDock" -Force -ErrorAction Stop + Start-Sleep -Seconds 2 + Write-Host " SUCCESS: StreamDock process stopped" -ForegroundColor Green + } catch { + Write-Host " WARNING: Failed to stop process (permission issue)" -ForegroundColor Yellow + Write-Host " Continuing... Please close StreamDock manually." -ForegroundColor Yellow + Write-Host "" -ForegroundColor Yellow + Write-Host " => Right-click StreamDock icon in taskbar -> Exit" -ForegroundColor Cyan + Write-Host " => Press any key to continue after closing..." -ForegroundColor Cyan + $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") + Write-Host "" + } +} else { + Write-Host " INFO: StreamDock process is not running" -ForegroundColor Gray +} + +Write-Host "" + +# Step 2: Backup and remove existing plugin folder +Write-Host "Step 2: Cleaning up existing plugin folder..." -ForegroundColor Yellow + +if (Test-Path $PluginDestPath) { + # Create backup folder + $BackupPath = "$PluginDestPath.backup_$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Write-Host " Backing up existing plugin: $BackupPath" -ForegroundColor Gray + + try { + Move-Item -Path $PluginDestPath -Destination $BackupPath -Force + Write-Host " SUCCESS: Existing plugin backed up" -ForegroundColor Green + } catch { + Write-Host " WARNING: Backup failed, attempting to delete..." -ForegroundColor Yellow + Remove-Item -Path $PluginDestPath -Recurse -Force -ErrorAction SilentlyContinue + } +} else { + Write-Host " INFO: No existing plugin found" -ForegroundColor Gray +} + +# Ensure parent directory exists +$PluginDestParent = Split-Path -Parent $PluginDestPath +if (-not (Test-Path $PluginDestParent)) { + Write-Host " Creating plugin directory: $PluginDestParent" -ForegroundColor Gray + New-Item -ItemType Directory -Path $PluginDestParent -Force | Out-Null +} + +Write-Host "" + +# Step 3: Copy new plugin +Write-Host "Step 3: Copying new plugin..." -ForegroundColor Yellow + +if (-not (Test-Path $PluginSourcePath)) { + Write-Host " ERROR: Source plugin folder not found: $PluginSourcePath" -ForegroundColor Red + exit 1 +} + +try { + Write-Host " Copying from: $PluginSourcePath" -ForegroundColor Gray + Write-Host " Copying to: $PluginDestPath" -ForegroundColor Gray + + Copy-Item -Path $PluginSourcePath -Destination $PluginDestPath -Recurse -Force + + # Count copied files + $CopiedFiles = Get-ChildItem -Path $PluginDestPath -Recurse -File + Write-Host " SUCCESS: Plugin copied ($($CopiedFiles.Count) files)" -ForegroundColor Green + +} catch { + Write-Host " ERROR: Plugin copy failed: $_" -ForegroundColor Red + exit 1 +} + +Write-Host "" + +# Step 4: Restart StreamDock +Write-Host "Step 4: Restarting StreamDock..." -ForegroundColor Yellow + +if ($StreamDeckExeFound) { + try { + Start-Process -FilePath $StreamDeckExe + Write-Host " StreamDock started: $StreamDeckExe" -ForegroundColor Gray + + # Wait for process to start + Start-Sleep -Seconds 3 + + $NewProcess = Get-Process -Name "StreamDock" -ErrorAction SilentlyContinue + if ($NewProcess) { + Write-Host " SUCCESS: StreamDock restarted (PID: $($NewProcess.Id))" -ForegroundColor Green + } else { + Write-Host " WARNING: Cannot verify StreamDock process (may be running in background)" -ForegroundColor Yellow + } + + } catch { + Write-Host " ERROR: StreamDock restart failed: $_" -ForegroundColor Red + Write-Host " Please start StreamDock manually." -ForegroundColor Yellow + } +} else { + Write-Host " WARNING: StreamDock executable not found, skipping auto-restart." -ForegroundColor Yellow + Write-Host " Please restart StreamDock manually:" -ForegroundColor Yellow + Write-Host " 1. Right-click StreamDock icon in taskbar -> Exit" -ForegroundColor Gray + Write-Host " 2. Search 'StreamDock' in Start Menu and run" -ForegroundColor Gray +} + +Write-Host "" +Write-Host "========================================" -ForegroundColor Cyan +Write-Host " Deployment Complete!" -ForegroundColor Green +Write-Host "========================================" -ForegroundColor Cyan +Write-Host "" +Write-Host "Plugin has been updated." -ForegroundColor Green +Write-Host "Check new actions in StreamDeck!" -ForegroundColor Green +Write-Host "" + +# Auto-close after 5 seconds +Write-Host "Auto-closing in 5 seconds..." -ForegroundColor Gray +Start-Sleep -Seconds 5 diff --git a/Streamdeck/view-logs.bat b/Streamdeck/view-logs.bat new file mode 100644 index 00000000..d5d023aa --- /dev/null +++ b/Streamdeck/view-logs.bat @@ -0,0 +1,45 @@ +@echo off +chcp 65001 >nul + +echo ======================================== +echo StreamDock Plugin Log Viewer +echo ======================================== +echo. + +echo Opening StreamDock log directory... +echo. + +set LOG_DIR=%APPDATA%\Hotspot\StreamDock\logs + +if exist "%LOG_DIR%" ( + echo Log directory found: %LOG_DIR% + echo. + echo Recent log files: + dir /B /O-D "%LOG_DIR%\*.log" 2>nul | findstr /N "^" + echo. + echo Opening log directory in Explorer... + explorer "%LOG_DIR%" + echo. + echo Opening latest log file in Notepad... + for /f "delims=" %%i in ('dir /B /O-D "%LOG_DIR%\*.log" 2^>nul') do ( + start notepad "%LOG_DIR%\%%i" + goto :done + ) +) else ( + echo WARNING: Log directory not found! + echo Checking alternative locations... + echo. + + set ALT_LOG=%LOCALAPPDATA%\Hotspot\StreamDock\logs + if exist "!ALT_LOG!" ( + echo Found at: !ALT_LOG! + explorer "!ALT_LOG!" + ) else ( + echo No logs found. StreamDock may not have been launched yet. + ) +) + +:done +echo. +echo Press any key to exit... +pause >nul