371 lines
18 KiB (Stored with Git LFS)
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 필드명 불일치 | 콘솔의 `필드를 찾을 수 없음` 경고 확인 |