# KindRetargeting OptiTrack 모션캡처 데이터를 임의의 Humanoid 아바타에 실시간 리타게팅하는 **자체 구현 파이프라인**. 자체 Two-Bone IK, 상황 기반 IK 가중치 자동화, OptiTrack 스파인/넥 분배, 손가락 머슬 제어, 머리 회전/크기 보정, 다중 아바타 동기화, WebSocket 원격 제어를 포함한다. > FinalIK 등 외부 IK 패키지에 의존하지 않는다. IK는 `TwoBoneIKSolver`로 직접 구현되어 있다. --- ## 폴더 구조 ``` KindRetargeting/ ├── CustomRetargetingScript.cs ← 메인 허브 (전신 리타게팅 + 캘리브레이션 + 설정 저장/로드) ├── TwoBoneIKSolver.cs ← 자체 Two-Bone IK 솔버 (4사지, FinalIK 미사용) ├── LimbWeightController.cs ← 거리/상황 기반 IK 가중치 자동화 ├── FingerShapedController.cs ← 손가락 포즈 수동 제어 (HumanPose Muscle 기반) ├── PropLocationController.cs ← 프랍 부착점 관리 (손/머리 Target-Offset 계층) ├── SimplePoseTransfer.cs ← 1:N 아바타 포즈 동기화 (독립 컴포넌트) ├── OffsetTransfer.cs ← 범용 오프셋 포즈 복사 유틸 (독립 컴포넌트) ├── IKTargetGizmo.cs ← IK 타겟 기즈모 시각화 ├── PropTypeController.cs ← 프랍 종류 분류 컴포넌트 (None/Object/Chair/Hand) ├── Enums/ │ └── RetargetingEnums.cs ← FingerCopyMode, PropType 열거형 ├── Remote/ │ ├── RetargetingRemoteController.cs ← WebSocket 원격 제어 (포트 64212) │ └── RetargetingWebSocketServer.cs ← websocket-sharp 서버 구현 (/retargeting) └── Editor/ ├── BaseRetargetingEditor.cs ← 공통 에디터 베이스 (주기적 갱신) ├── CustomRetargetingScriptEditor.cs ← 메인 Inspector UI ├── SimplePoseTransferEditor.cs ← 포즈 전송 Inspector (UXML 기반) ├── RetargetingRemoteControllerEditor.cs ← 원격 제어 Inspector ├── RetargetingControlWindow.cs ← 전용 에디터 윈도우 (Tools/리타게팅 컨트롤 패널) └── UXML/ └── SimplePoseTransferEditor.uxml ``` > 과거 문서에 있던 `ShoulderCorrectionFunction.cs`(어깨 보정)와 `FootGroundingController.cs`(2-Pass 발 접지)는 > **현재 코드에 존재하지 않는다.** 어깨 보정 기능은 제거되었고, 발 접지는 1€ 필터 raw 위치 + `floorHeight` > + `LimbWeightController`의 발 높이 가중치 조합으로 대체되었다. --- ## 아키텍처 개요 ``` OptitrackSkeletonAnimator_Mingle (소스 데이터, DefaultExecutionOrder -100) │ GetBoneTransform(HumanBodyBones) / TryGetRawWorldPosition·Rotation / GetSpine·NeckChainTransforms ↓ CustomRetargetingScript (메인 허브) ├── 회전 오프셋 리타게팅 ← offset = Inv(source) * target 캐싱·적용 ├── 힙 높이 자동 보정 ← 매 프레임 (타겟다리 − 소스다리) 월드 Y, 앉기 가중치 반영 ├── OptiTrack 스파인/넥 분배 ← 소스 다본 → 타겟 Spine/Chest/UpperChest/Neck (가상 본 그룹핑) ├── TwoBoneIKSolver ← 자체 4사지 IK ├── LimbWeightController ← 다층 IK 가중치 자동화 ├── FingerShapedController ← HumanPose Muscle 손가락 제어 ├── PropLocationController ← 손/머리 부착점 생성/관리 └── 머리 회전/크기 보정 (LateUpdate) SimplePoseTransfer ← 독립 컴포넌트, 1개 소스 → N개 아바타 동기화 OffsetTransfer ← 독립 컴포넌트, 범용 1:1 오프셋 포즈 복사 RetargetingRemoteController ← WebSocket(64212)으로 원격 파라미터 제어 ``` --- ## 실행 순서 | 타이밍 | 스크립트 | 작업 | |---|---|---| | `Update` (Order -100) | `OptitrackSkeletonAnimator_Mingle` | Motive → Transform 적용 (+ 1€ 필터, raw 보관) | | `Update` (미지정) | `CustomRetargetingScript` | 포즈 복사 → 스파인/넥 분배 → IK 타겟 갱신 → 손가락 → 가중치 → IK 솔브 | | `LateUpdate` (미지정) | `CustomRetargetingScript` | 머리 회전 오프셋 + 머리 크기 적용 (IK 이후) | | `LateUpdate` (Order 16001) | `SimplePoseTransfer` | 멀티 아바타 동기화 | > 메인 허브의 본체 리타게팅·IK는 `LateUpdate`가 아니라 **`Update`**에서 실행되며, > `LateUpdate`에서는 머리 보정만 처리한다. --- ## 핵심 스크립트 상세 ### CustomRetargetingScript — 메인 허브 전신 리타게팅, 캘리브레이션, 설정 저장/로드, 스케일·머리 보정을 담당하는 핵심 컴포넌트. **리타게팅 기본 원리:** - 캘리브레이션(T/I-포즈) 시 본별 `offset = Inv(source.rotation) * target.rotation` 계산 후 캐시 - 런타임: `target.rotation = source.rotation * offset` (본 0~54, 손가락 포함) - 힙 회전은 오프셋 적용해 동기화, 힙 위치는 소스 위치 + 다리 높이 자동 보정(아래 참조) **힙 높이 자동 보정 (수동 힙 위치 보정 없음):** - 수동 힙 오프셋(`hipsOffsetX/Y/Z`)은 제거되었다. 힙 위치는 소스 힙 위치를 직접 사용한다. - 매 프레임 `(타겟 왼다리 길이 − 소스 왼다리 길이)`를 월드 Y에 더해 발 접지를 맞춘다 (`ComputeLegHeightOffset()`). 타겟 다리가 길수록 힙을 위로 올린다. - 앉기 가중치 `HipsWeightOffset`(LimbWeight)를 곱하므로 의자에 앉을 때는 보정이 줄어든다. - 다리 분절 길이는 포즈와 무관하게 일정하므로 매 프레임 계산해도 안전하며, `avatarScale`을 바꾸면 별도 재캘리브 없이 즉시 반영된다. **Inspector 주요 파라미터:** | 헤더 | 파라미터 | 설명 | |---|---|---| | 발 IK | `footFrontBackOffset` | 발 앞뒤 오프셋 (발 로컬 z) | | 발 IK | `footInOutOffset` | 발 벌리기/모으기 (발 로컬 x, 좌우 부호 반전) | | 바닥 | `floorHeight` | 힙·발 IK 타겟에 더해지는 월드 Y 오프셋 | | 머리 회전 | `headRotationOffsetX/Y/Z` | 머리 로컬 회전 보정 (-180~180°) | | 크기 | `avatarScale` | 아바타 전체 크기 (0.1~3) | | 크기 | `headScale` | 머리 크기 독립 조정 (0.1~3) | **캘리브레이션 / 설정 메서드:** ```csharp void I_PoseCalibration() // I-포즈에서 회전 오프셋 캐싱 (손가락 포즈는 보존) void ResetPoseAndCache() // 캐시 삭제 + T-포즈 복원 후 재계산 void CalibrateHeadToForward() // 현재 머리 방향을 T-포즈 정면 방향으로 보정 void SaveSettings() // JSON 저장 (OnApplicationQuit 시 자동 저장) void LoadSettings() // 저장된 JSON 로드 bool HasCachedSettings() // 저장된 설정 파일 존재 여부 ``` **외부 접근 API:** ```csharp float GetAvatarScale(); void SetAvatarScale(float); void ResetScale(); float GetHeadScale(); void SetHeadScale(float); void ResetHeadScale(); void SetFingerShapedEnabled(bool); ``` **설정 저장 경로:** ``` %LocalAppData%\Unity\{Application.companyName}\{Application.productName}\RetargetingSettings\{타겟이름}_settings.json ``` 저장 항목: 발 오프셋, floorHeight, 회전 오프셋 캐시, initialHipsHeight, avatarScale, headScale, chairSeatHeightOffset, 머리 회전 오프셋. (힙 위치 오프셋·무릎 위치 조정은 제거됨 — 힙 높이는 런타임 다리 길이 자동 보정으로 처리, 무릎은 IK가 소스 무릎 투영을 쓰므로 bend goal 조정이 불필요) --- ### OptiTrack 스파인/넥 분배 소스 OptiTrack 스켈레톤은 스파인/넥 본 개수가 타겟 Humanoid와 다를 수 있다(예: 소스 5본 ↔ 타겟 Spine/Chest/UpperChest 3본). 이를 **가상 본 그룹핑**으로 매핑한다. **원리:** 1. 초기화 시 소스 체인(`GetSpineChainTransforms`/`GetNeckChainTransforms`)을 타겟 본 수만큼 그룹으로 분할 2. 각 타겟 본이 담당하는 소스 그룹의 **마지막 본** 월드 회전을 "가상 본 회전"으로 사용 3. T-포즈 기준 `offset = Inv(가상본 월드회전) * 타겟본 월드회전` 선계산 4. 런타임: `타겟본.rotation = 가상본 월드회전 * offset` 분배 대상 본(Spine/Chest/UpperChest/Neck)은 일반 `SyncBoneRotations`에서 제외된다. 소스 체인이 비어 있으면 자동 비활성화되고 일반 회전 복사로 폴백한다. --- ### TwoBoneIKSolver — 자체 IK 솔버 FinalIK 등 외부 패키지 없이 직접 구현한 Two-Bone IK. 양팔/양다리 4개 사지에 적용. **LimbIK 구조:** ```csharp class LimbIK { Transform target; // IK 목표 위치/회전 Transform bendGoal; // 무릎/팔꿈치 방향 힌트 float positionWeight; // 위치 IK 가중치 (FK↔IK 블렌딩) float rotationWeight; // end 본 회전 IK 가중치 float bendGoalWeight; // bend goal 가중치 // 캐시: upper/lower/end, upperLength/lowerLength, localBendNormal // 소스 참조: sourceUpper/sourceLower/sourceEnd (다리에만 설정됨) } ``` **무릎/팔꿈치 위치 결정 — 두 가지 방식:** - **다리 (소스 참조 있음, `ComputeKneePosFromSource`)**: 소스 무릎 위치를 소스 hip→foot 직선 기준으로 투영(projection)+수직(rejection) 성분으로 분해 → 타겟 체인 길이 비율로 스케일 → 소스/타겟 사지 방향 차이만큼 수직 성분 회전 → 타겟 무릎 배치. **코사인 법칙을 쓰지 않으므로 180° 특이점이 없고 역관절도 자연스럽게 보존**된다. - **팔 (소스 참조 없음, `ComputeKneePosFromBendGoal`)**: bendGoal 수직 성분 + T-포즈 기반 기본 방향 + 코사인 법칙으로 팔꿈치 위치 계산. **블렌딩:** upper/lower 회전을 FK ↔ IK 사이에서 `positionWeight`로 Slerp, end 회전은 `rotationWeight`로 Slerp. weight가 0이면 IK 스킵(완전 FK). **`CalculateAutoFloorHeight(comfortRatio)`:** 왼다리 길이 기반 자동 바닥 높이 계산. --- ### LimbWeightController — IK 가중치 자동화 상황에 따라 IK 가중치를 자동 조절. 다층 레이어를 합성한 뒤 `weightSmoothSpeed`로 Lerp 스무딩하여 급격한 IK 전환을 방지한다. **가중치 레이어:** | 인덱스 | 팔 (`*ArmEndWeights`) | 다리 (`*LegEndWeights`) | 힙 (`hipsWeights`) | |---|---|---|---| | `[0]` | 양손 간 거리 (악수/박수) | 앉기 시 발-힙 수평거리 | 의자 프랍과의 거리 | | `[1]` | 프랍과의 최소 거리 | 발 높이 | 바닥 기준 힙 높이 | **합성 방식:** - 팔: `GetMaxValue` — 어느 조건이든 하나라도 해당되면 IK 활성화 - 다리/힙: `GetMinValue` — 모든 조건이 만족할 때만 IK 활성화 **props 자동 수집 (Initialize 시):** - 씬의 모든 `PropTypeController` 부착 오브젝트 - 씬 내 다른 캐릭터의 손(LeftHand/RightHand) Transform **의자 앉기 처리 (`SitChairDistances`):** - `PropType.Chair` 프랍과 힙 거리 계산 → 가까울수록 `hipsWeights[0]` 감소 - 가까울수록 `chairSeatHeightOffset`를 비례 적용 → `crs.ChairSeatHeightOffset`로 전달되어 힙 Y 보정 **최종 적용:** `ApplyWeightsToFBIK`에서 각 LimbIK의 position/rotation/bendGoal weight 설정. `enableLeftArmIK`/`enableRightArmIK`로 팔 IK를 강제 끌 수 있다. --- ### FingerShapedController — 손가락 포즈 HumanPose Muscle 값으로 손가락 포즈를 수동 제어. **작동 원리 (트릭):** 1. `SetHumanPose` 호출 전, 손가락 외 24개 본의 로컬 회전 저장 2. `HumanPoseHandler.SetHumanPose()`로 손가락 머슬 적용 (전신에 영향) 3. 저장했던 비손가락 본 회전 즉시 복원 → 몸 포즈/본 길이 변형 방지 **Muscle 인덱스 베이스:** 왼손 `muscles[55]`~, 오른손 `muscles[75]`~ 각 손가락 4개 머슬(curl 3 + spread 1) 구조. **제어 파라미터 (모두 -1~1):** ``` left/right + ThumbCurl, IndexCurl, MiddleCurl, RingCurl, PinkyCurl, SpreadFingers ``` `enabled`, `leftHandEnabled`, `rightHandEnabled`로 손별 on/off. --- ### PropLocationController — 프랍 부착점 T-포즈 기준으로 손/머리에 Target-Offset 계층을 생성하고 프랍을 부착. **생성 계층:** ``` [손 본] └── Left_Hand_Target (위치: 손 본 + 오프셋, 회전: Euler(90,0,0)) └── Left_Hand_Offset (로컬 zero) └── [부착된 프랍] ``` **오프셋 기본값:** 왼손 `(-0.039, -0.022, 0)`, 오른손 `(+0.039, -0.022, 0)`, 머리 `(0, 0.16, 0)` **API:** ```csharp void MoveToLeftHand(GameObject) / MoveToRightHand(GameObject) / MoveToHead(GameObject) void DetachProp(GameObject) Transform GetLeftHandOffset() / GetRightHandOffset() / GetHeadOffset() GameObject[] GetLeftHandProps() / GetRightHandProps() / GetHeadProps() ``` (에디터에서는 인자 없는 오버로드가 `Selection.activeGameObject`를 사용) --- ### SimplePoseTransfer — 멀티 아바타 동기화 1개 소스 Animator의 포즈를 N개 타겟 Animator에 복사. `CustomRetargetingScript`와 독립 동작. (Order 16001) **TargetEntry 구조:** `animator`, `hipOffset`(월드 공간), `syncRootScale`(타겟별 on/off) **초기화:** 타겟별로 55개 본에 대해 T-포즈 기준 회전 차이 선계산 ```csharp boneRotationDifferences[t, i] = Quaternion.Inverse(source.rotation) * target.rotation ``` **런타임 (LateUpdate):** ```csharp target.rotation = source.rotation * boneRotationDifferences[t, i] // Hips(0): position = source.position + hipOffset // 루트 회전 동기화 + (옵션) 루트 스케일 비율 동기화 ``` **추가 기능:** - `transferHeadScale`: 소스의 `CustomRetargetingScript.GetHeadScale()`을 타겟 머리에 동기화 (없으면 소스 머리 localScale 직접 복사) - `syncRootScale`: 소스 루트 스케일 변화 비율을 타겟 원본 스케일에 곱해 적용 --- ### OffsetTransfer — 범용 오프셋 포즈 복사 유틸 소스/타겟 Animator 간 1:1 포즈 복사용 독립 컴포넌트. 본 0~54에 대해 `offset = Inv(source) * target`을 계산하고, `target.rotation = source.rotation * offset`, `target.position = source.position`을 복사한다. 메인 파이프라인이 직접 사용하진 않지만, 스파인/넥 분배가 이 "OffsetTransfer 패턴"을 따른다. --- ### RetargetingRemoteController — WebSocket 원격 제어 **포트:** `64212` (StreamDeck 서버 `64211`과 별도) · 경로 `/retargeting` · 메인 스레드 액션 큐로 Unity API 호출 **지원 액션:** | action | 설명 | |---|---| | `refresh` | 등록 캐릭터 목록 + 손 포즈 프리셋 전송 | | `getCharacterData` | 특정 캐릭터의 모든 파라미터 조회 | | `updateValueRealtime` | 실시간 단일 파라미터 변경 + 변경 브로드캐스트 | | `setHandPosePreset` | 손 포즈 프리셋 적용 | | `calibrateIPose` | I-포즈 캘리브레이션 | | `resetCalibration` | 캘리브레이션 초기화 | | `calibrateHeadForward` | 머리 정면 캘리브레이션 | | `autoCalibrateAll` | 전체 자동 보정 (크기 + 머리) | > `autoHipsOffset` 액션은 제거됨 — 힙 높이는 매 프레임 다리 길이 자동 보정이 처리한다. **손 포즈 프리셋:** `가위`, `바위`, `보`, `브이`, `검지`, `초기화` **autoCalibrateAll 순서 (코루틴):** 1. `ResetScale()` + avatarScale=1 2. 1프레임 대기 3. 목 높이 비율(`sourceNeck.y / targetNeck.y`, 0.1~3 클램프)로 avatarScale 계산 4. 1프레임 대기 5. `CalibrateHeadToForward()` + 저장 (힙 높이는 런타임 자동 보정) **주의:** private 필드 접근에 Reflection(`BindingFlags.NonPublic`)을 사용한다 (`avatarScale`, `fingerCopyMode` 등). 필드명을 바꾸면 원격 제어가 무음 실패하므로 `GetPrivateField`/`SetPrivateField`의 경고 로그를 확인할 것. --- ## 열거형 ```csharp // RetargetingEnums.cs — namespace KindRetargeting.EnumsList enum FingerCopyMode { None, MuscleData, Rotation } // 손가락 복제 방식 enum PropType { None, Object, Chair, Hand } // 프랍 분류 ``` --- ## 씬 설정 체크리스트 ### 단일 캐릭터 - [ ] `OptitrackStreamingClient` 배치 (포트/IP) - [ ] `OptitrackSkeletonAnimator_Mingle` 배치 + 본 매핑 생성 - [ ] `CustomRetargetingScript` 배치 - `optitrackSource` 연결 - 소스 하위에 `IK` 루트(LeftLowerLeg/RightLowerLeg/LeftLowerArm/RightLowerArm) 존재 확인 - `ikSolver`, `limbWeight`, `fingerShaped`, `propLocation` 설정 - [ ] `RetargetingRemoteController` 배치 + `registeredCharacters`에 캐릭터 등록 ### 프랍 - [ ] 프랍에 `PropTypeController` 추가 + `propType` 설정 - [ ] 의자는 `PropType.Chair` (앉기 가중치 + 좌석 높이 보정) - [ ] 리짓바디 추적 프랍은 `OptitrackRigidBody` 추가 + `propName` 설정 ### 멀티 아바타 - [ ] `SimplePoseTransfer` 배치 - `sourceBone` = 리타게팅된 메인 아바타 Animator - `targets[]` = 동기화할 아바타 + 옵션(hipOffset, syncRootScale) --- ## 자주 발생하는 문제 | 증상 | 원인 | 해결 | |---|---|---| | 아바타가 전혀 안 움직임 | `optitrackSource` 미연결 / 스켈레톤 미감지 | StreamingClient IP·본 매핑 확인 | | `IK 루트를 찾을 수 없습니다` 에러 | 소스 하위 `IK` 오브젝트/하위 본 누락 | 소스 프리펩의 IK 계층 확인 | | 힙이 떠있음/파묻힘 | 다리 길이 자동 보정 오작동 (소스/타겟 다리 본 누락) | 다리 본 매핑 확인, `floorHeight`로 미세 조정 | | 무릎/팔꿈치 방향 이상 | 다리는 소스 IK 본, 팔은 bendGoal 기준 | 소스 IK 본 위치 확인 (무릎 수동 조정은 제거됨) | | 손가락이 이상하게 꺾임 | 머슬 인덱스/활성화 문제 | `fingerShaped.enabled`·손별 enable 확인 | | 발이 바닥을 뚫거나 뜸 | `floorHeight` / 발 높이 가중치 | `floorHeight`, `footHeight*Threshold` 조정 | | 멀티 아바타 포즈 틀어짐 | `SimplePoseTransfer` Init 타이밍 | Play 후 `Init()` 재호출 / 실행 순서 확인 | | 원격 파라미터 변경 안 됨 | Reflection 필드명 불일치 | 콘솔의 `필드를 찾을 수 없음` 경고 확인 |