Fix : 유니티에 의한 가변 프레임 버그 해결
This commit is contained in:
parent
71b9521372
commit
b4044a90f5
@ -37,7 +37,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
|
||||
[Header("본 1€ 필터 (속도 적응형 저역통과)")]
|
||||
[HideInInspector]
|
||||
public FilterStrength filterStrength = FilterStrength.Medium;
|
||||
public FilterStrength filterStrength = FilterStrength.Off;
|
||||
|
||||
[Header("어깨 증폭")]
|
||||
[Tooltip("어깨 회전을 증폭합니다. 1 = 원본, 2 = 2배. 하위 체인(상완)은 자동 역보정되어 손 위치가 유지됩니다.")]
|
||||
@ -60,6 +60,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
|
||||
[HideInInspector] public bool enableBoneFilter = true;
|
||||
|
||||
[Header("프레임 보간")]
|
||||
[Tooltip("OptiTrack 프레임 사이를 보간하여 Unity 가변 프레임에서도 부드러운 모션을 생성합니다. 약 1프레임(~8ms @120fps) 지연이 추가됩니다.")]
|
||||
public bool enableInterpolation = true;
|
||||
|
||||
[Tooltip("보간 지연 시간(초). 0이면 자동(OptiTrack 프레임 간격 사용). 높을수록 부드럽지만 지연 증가.")]
|
||||
[Range(0f, 0.05f)]
|
||||
public float interpolationDelay = 0f;
|
||||
|
||||
/// <summary>
|
||||
/// 런타임에서 필터 강도를 변경합니다. StreamDeck/핫키 등에서 호출.
|
||||
/// </summary>
|
||||
@ -148,6 +156,16 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
}
|
||||
private Dictionary<Int32, BoneFilterState> m_filterStates = new Dictionary<Int32, BoneFilterState>();
|
||||
|
||||
// 프레임 보간용 이중 버퍼 (prev/curr OptiTrack 프레임)
|
||||
private Dictionary<Int32, Vector3> m_interpPrevPos = new Dictionary<Int32, Vector3>();
|
||||
private Dictionary<Int32, Quaternion> m_interpPrevOri = new Dictionary<Int32, Quaternion>();
|
||||
private Dictionary<Int32, Vector3> m_interpCurrPos = new Dictionary<Int32, Vector3>();
|
||||
private Dictionary<Int32, Quaternion> m_interpCurrOri = new Dictionary<Int32, Quaternion>();
|
||||
private OptitrackHiResTimer.Timestamp m_interpPrevTs;
|
||||
private OptitrackHiResTimer.Timestamp m_interpCurrTs;
|
||||
private bool m_interpHasCurr = false;
|
||||
private bool m_interpReady = false;
|
||||
|
||||
// OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑
|
||||
public static readonly Dictionary<string, string> DefaultOptiToFbxSuffix = new Dictionary<string, string>
|
||||
{
|
||||
@ -319,11 +337,12 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
// 필터 활성화 상태 동기화 (프리셋 값은 SetFilterStrength()에서만 적용)
|
||||
enableBoneFilter = filterStrength != FilterStrength.Off;
|
||||
|
||||
// MirrorMode 변경 감지 → 필터 상태 리셋 (불연속 튐 방지)
|
||||
// MirrorMode 변경 감지 → 필터/보간 상태 리셋 (불연속 튐 방지)
|
||||
bool currentMirrorMode = StreamingClient != null && StreamingClient.MirrorMode;
|
||||
if (currentMirrorMode != m_lastMirrorMode)
|
||||
{
|
||||
m_filterStates.Clear();
|
||||
ClearInterpolationBuffers();
|
||||
m_lastMirrorMode = currentMirrorMode;
|
||||
}
|
||||
|
||||
@ -335,6 +354,7 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
previousSkeletonName = SkeletonAssetName;
|
||||
if (m_skeletonDef != null)
|
||||
RebuildBoneIdMapping();
|
||||
ClearInterpolationBuffers();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -355,6 +375,10 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
m_lastFrameTimestamp = frameTs;
|
||||
m_hasLastFrameTimestamp = true;
|
||||
|
||||
// ── 프레임 보간: 두 OptiTrack 프레임 사이를 시간 기반으로 Lerp/Slerp ──
|
||||
if (enableInterpolation)
|
||||
InterpolateSnapshots(frameTs);
|
||||
|
||||
// ── Pass 1: Raw 데이터 적용 → IK 포인트 월드 위치 캡처 ──────────────────
|
||||
// 필터가 활성화되어 있을 때만 two-pass, 비활성이면 single-pass
|
||||
if (enableBoneFilter)
|
||||
@ -625,6 +649,81 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
|
||||
}
|
||||
}
|
||||
|
||||
// ── 프레임 보간 (Frame Interpolation) ────────────────────────────────────
|
||||
// OptiTrack(고정 120fps)과 Unity(가변 프레임) 사이의 타이밍 불일치로 인한 떨림을 제거.
|
||||
// 이전/현재 두 프레임을 버퍼링하고 하드웨어 타임스탬프 기반으로 Lerp/Slerp.
|
||||
// 약 1 OptiTrack 프레임(~8ms @120fps)의 지연이 추가되지만 모션이 매끄러워짐.
|
||||
|
||||
/// <summary>
|
||||
/// 보간 버퍼를 초기화합니다. MirrorMode 전환, 스켈레톤 변경 등 불연속 시점에 호출.
|
||||
/// </summary>
|
||||
private void ClearInterpolationBuffers()
|
||||
{
|
||||
m_interpPrevPos.Clear();
|
||||
m_interpPrevOri.Clear();
|
||||
m_interpCurrPos.Clear();
|
||||
m_interpCurrOri.Clear();
|
||||
m_interpHasCurr = false;
|
||||
m_interpReady = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 두 OptiTrack 프레임 사이를 시간 기반으로 보간합니다.
|
||||
/// m_snapshotPositions/Orientations를 보간된 결과로 덮어씁니다.
|
||||
/// </summary>
|
||||
private void InterpolateSnapshots(OptitrackHiResTimer.Timestamp frameTs)
|
||||
{
|
||||
// 새 프레임 감지 (타임스탬프가 변경되었으면 새 OptiTrack 프레임이 도착한 것)
|
||||
if (frameTs.m_ticks != m_interpCurrTs.m_ticks)
|
||||
{
|
||||
// curr → prev 스왑 (딕셔너리 참조 교환으로 GC 방지)
|
||||
(m_interpPrevPos, m_interpCurrPos) = (m_interpCurrPos, m_interpPrevPos);
|
||||
(m_interpPrevOri, m_interpCurrOri) = (m_interpCurrOri, m_interpPrevOri);
|
||||
m_interpPrevTs = m_interpCurrTs;
|
||||
|
||||
// 새 스냅샷 → curr 복사
|
||||
m_interpCurrPos.Clear();
|
||||
m_interpCurrOri.Clear();
|
||||
foreach (var kvp in m_snapshotPositions)
|
||||
m_interpCurrPos[kvp.Key] = kvp.Value;
|
||||
foreach (var kvp in m_snapshotOrientations)
|
||||
m_interpCurrOri[kvp.Key] = kvp.Value;
|
||||
m_interpCurrTs = frameTs;
|
||||
|
||||
if (!m_interpReady && m_interpHasCurr)
|
||||
m_interpReady = true;
|
||||
m_interpHasCurr = true;
|
||||
}
|
||||
|
||||
if (!m_interpReady) return;
|
||||
|
||||
// 프레임 간격 계산
|
||||
float frameDuration = m_interpCurrTs.SecondsSince(m_interpPrevTs);
|
||||
if (frameDuration < 0.001f || frameDuration > 0.1f) return; // 비정상 간격 무시
|
||||
|
||||
// 보간 계수: target_time = now - delay → 항상 prev~curr 사이에서 보간
|
||||
float delay = interpolationDelay > 0f ? interpolationDelay : m_natNetDt;
|
||||
float timeSincePrev = OptitrackHiResTimer.Now().SecondsSince(m_interpPrevTs);
|
||||
float t = Mathf.Clamp01((timeSincePrev - delay) / frameDuration);
|
||||
|
||||
// m_snapshotPositions/Orientations를 보간 결과로 덮어쓰기
|
||||
m_snapshotPositions.Clear();
|
||||
m_snapshotOrientations.Clear();
|
||||
|
||||
foreach (var kvp in m_interpCurrPos)
|
||||
{
|
||||
m_snapshotPositions[kvp.Key] = m_interpPrevPos.TryGetValue(kvp.Key, out Vector3 prevP)
|
||||
? Vector3.Lerp(prevP, kvp.Value, t)
|
||||
: kvp.Value;
|
||||
}
|
||||
foreach (var kvp in m_interpCurrOri)
|
||||
{
|
||||
m_snapshotOrientations[kvp.Key] = m_interpPrevOri.TryGetValue(kvp.Key, out Quaternion prevO)
|
||||
? Quaternion.Slerp(prevO, kvp.Value, t)
|
||||
: kvp.Value;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 1€ Filter (One Euro Filter) ──────────────────────────────────────────
|
||||
// 참고: Géry Casiez et al., "1€ Filter: A Simple Speed-based Low-pass Filter", CHI 2012
|
||||
// 속도가 빠를수록 cutoff 상승 → 지연 감소, 속도가 느릴수록 cutoff = minCutoff → 노이즈 제거
|
||||
|
||||
@ -51,6 +51,7 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
private readonly List<Camera> previewCameraPool = new List<Camera>();
|
||||
private RenderTexture previewRT;
|
||||
private Texture2D previewReadbackTexture;
|
||||
private int previewTextureVersion = 0;
|
||||
private int currentPreviewIndex = 0;
|
||||
private int previewFrameCounter = 0;
|
||||
private readonly Dictionary<Camera, int> previewPendingCaptures = new Dictionary<Camera, int>();
|
||||
@ -66,26 +67,26 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
|
||||
void Start()
|
||||
{
|
||||
cameraManager = FindObjectOfType<CameraManager>();
|
||||
cameraManager = FindAnyObjectByType<CameraManager>();
|
||||
if (cameraManager == null)
|
||||
{
|
||||
Debug.LogError("[StreamDeckServerManager] CameraManager를 찾을 수 없습니다!");
|
||||
return;
|
||||
}
|
||||
|
||||
itemController = FindObjectOfType<ItemController>();
|
||||
itemController = FindAnyObjectByType<ItemController>();
|
||||
if (itemController == null)
|
||||
Debug.LogWarning("[StreamDeckServerManager] ItemController를 찾을 수 없습니다. 아이템 컨트롤 기능이 비활성화됩니다.");
|
||||
|
||||
eventController = FindObjectOfType<EventController>();
|
||||
eventController = FindAnyObjectByType<EventController>();
|
||||
if (eventController == null)
|
||||
Debug.LogWarning("[StreamDeckServerManager] EventController를 찾을 수 없습니다. 이벤트 컨트롤 기능이 비활성화됩니다.");
|
||||
|
||||
avatarOutfitController = FindObjectOfType<AvatarOutfitController>();
|
||||
avatarOutfitController = FindAnyObjectByType<AvatarOutfitController>();
|
||||
if (avatarOutfitController == null)
|
||||
Debug.LogWarning("[StreamDeckServerManager] AvatarOutfitController를 찾을 수 없습니다. 아바타 의상 컨트롤 기능이 비활성화됩니다.");
|
||||
|
||||
systemController = FindObjectOfType<SystemController>();
|
||||
systemController = FindAnyObjectByType<SystemController>();
|
||||
if (systemController == null)
|
||||
Debug.LogWarning("[StreamDeckServerManager] SystemController를 찾을 수 없습니다. 시스템 컨트롤 기능이 비활성화됩니다.");
|
||||
|
||||
@ -97,11 +98,21 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
|
||||
void Update()
|
||||
{
|
||||
// 큐를 락 안에서 드레인한 뒤, 락 밖에서 실행 — WebSocket 스레드 블로킹 최소화
|
||||
Action[] pendingActions = null;
|
||||
lock (lockObject)
|
||||
{
|
||||
while (mainThreadActions.Count > 0)
|
||||
if (mainThreadActions.Count > 0)
|
||||
{
|
||||
pendingActions = mainThreadActions.ToArray();
|
||||
mainThreadActions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (pendingActions != null)
|
||||
{
|
||||
foreach (var action in pendingActions)
|
||||
{
|
||||
var action = mainThreadActions.Dequeue();
|
||||
try
|
||||
{
|
||||
action?.Invoke();
|
||||
@ -129,10 +140,6 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
void OnDestroy()
|
||||
{
|
||||
CleanupPreview();
|
||||
}
|
||||
|
||||
void OnApplicationQuit()
|
||||
{
|
||||
StopServer();
|
||||
StopDashboardServer();
|
||||
}
|
||||
@ -145,7 +152,7 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
// 0.0.0.0 으로 바인딩하여 LAN 내 다른 기기에서도 접속 가능
|
||||
server = new WebSocketServer(port);
|
||||
server.KeepClean = true; // 비활성 세션 자동 정리
|
||||
server.WaitTime = TimeSpan.FromSeconds(3); // ping-pong 응답 대기 시간
|
||||
server.WaitTime = TimeSpan.FromSeconds(10); // ping-pong 응답 대기 시간 (모바일 WiFi 대응)
|
||||
server.AddWebSocketService<StreamDeckService>("/");
|
||||
server.Start();
|
||||
Debug.Log($"[StreamDeckServerManager] WebSocket 서버 시작됨, 포트: {port} (모든 인터페이스)");
|
||||
@ -221,14 +228,21 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
}
|
||||
|
||||
public void OnClientDisconnected(StreamDeckService service)
|
||||
{
|
||||
lock (lockObject)
|
||||
{
|
||||
mainThreadActions.Enqueue(() =>
|
||||
{
|
||||
connectedClients.Remove(service);
|
||||
PreviewUnsubscribe(service);
|
||||
Debug.Log($"[StreamDeckServerManager] 클라이언트 연결 해제됨. 총 연결: {connectedClients.Count}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public void BroadcastMessage(string message)
|
||||
{
|
||||
var deadClients = new List<StreamDeckService>();
|
||||
foreach (var client in connectedClients.ToArray())
|
||||
{
|
||||
try
|
||||
@ -238,9 +252,13 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"[StreamDeckServerManager] 메시지 전송 실패: {e.Message}");
|
||||
connectedClients.Remove(client);
|
||||
deadClients.Add(client);
|
||||
}
|
||||
}
|
||||
foreach (var dead in deadClients)
|
||||
{
|
||||
connectedClients.Remove(dead);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
@ -775,6 +793,8 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
|
||||
private void CreatePreviewRenderTextures()
|
||||
{
|
||||
previewTextureVersion++;
|
||||
|
||||
if (previewRT != null)
|
||||
{
|
||||
previewRT.Release();
|
||||
@ -815,8 +835,8 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
return;
|
||||
}
|
||||
|
||||
previewFrameCounter++;
|
||||
if (previewFrameCounter % renderInterval != 0)
|
||||
previewFrameCounter = (previewFrameCounter + 1) % renderInterval;
|
||||
if (previewFrameCounter != 0)
|
||||
return;
|
||||
|
||||
var presets = cameraManager.cameraPresets;
|
||||
@ -882,10 +902,13 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
|
||||
string presetName = presets[presetIndex].presetName;
|
||||
int capturedPresetIndex = presetIndex;
|
||||
int capturedTextureVersion = previewTextureVersion;
|
||||
|
||||
AsyncGPUReadback.Request(camera.targetTexture, 0, TextureFormat.RGB24, (request) =>
|
||||
{
|
||||
if (request.hasError || this == null) return;
|
||||
// 텍스처가 재생성되었으면 이 콜백의 데이터는 무효
|
||||
if (capturedTextureVersion != previewTextureVersion) return;
|
||||
|
||||
NativeArray<byte> data = request.GetData<byte>();
|
||||
previewReadbackTexture.LoadRawTextureData(data);
|
||||
@ -1051,19 +1074,24 @@ public class StreamDeckServerManager : MonoBehaviour
|
||||
{
|
||||
var statusData = new Dictionary<string, object>();
|
||||
|
||||
// OptiTrack
|
||||
if (systemController != null)
|
||||
{
|
||||
if (systemController.optiTrack != null)
|
||||
{
|
||||
statusData["optitrack"] = new
|
||||
{
|
||||
connected = systemController.optiTrack.IsOptitrackConnected(),
|
||||
status = systemController.optiTrack.GetOptitrackConnectionStatus()
|
||||
};
|
||||
}
|
||||
|
||||
if (systemController.facialMotion != null)
|
||||
{
|
||||
statusData["facial_motion"] = new
|
||||
{
|
||||
client_count = systemController.facialMotion.facialMotionClients?.Count ?? 0
|
||||
};
|
||||
}
|
||||
|
||||
statusData["recording"] = new
|
||||
{
|
||||
|
||||
@ -159,8 +159,20 @@ public class StreamingleDashboardServer
|
||||
try
|
||||
{
|
||||
HttpListenerContext context = listener.GetContext();
|
||||
// 요청을 ThreadPool에서 병렬 처리 — 동시 요청 블로킹 방지
|
||||
ThreadPool.QueueUserWorkItem(_ =>
|
||||
{
|
||||
try
|
||||
{
|
||||
ProcessRequest(context);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (isRunning)
|
||||
Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}");
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (HttpListenerException)
|
||||
{
|
||||
// 서버 종료 시 발생
|
||||
@ -169,7 +181,7 @@ public class StreamingleDashboardServer
|
||||
{
|
||||
if (isRunning)
|
||||
{
|
||||
Debug.LogError($"[StreamingleDashboard] 요청 처리 오류: {ex.Message}");
|
||||
Debug.LogError($"[StreamingleDashboard] 요청 수신 오류: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user