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