18 KiB (Stored with Git LFS)
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) |
캘리브레이션 / 설정 메서드:
void I_PoseCalibration() // I-포즈에서 회전 오프셋 캐싱 (손가락 포즈는 보존)
void ResetPoseAndCache() // 캐시 삭제 + T-포즈 복원 후 재계산
void CalibrateHeadToForward() // 현재 머리 방향을 T-포즈 정면 방향으로 보정
void SaveSettings() // JSON 저장 (OnApplicationQuit 시 자동 저장)
void LoadSettings() // 저장된 JSON 로드
bool HasCachedSettings() // 저장된 설정 파일 존재 여부
외부 접근 API:
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본). 이를 가상 본 그룹핑으로 매핑한다.
원리:
- 초기화 시 소스 체인(
GetSpineChainTransforms/GetNeckChainTransforms)을 타겟 본 수만큼 그룹으로 분할 - 각 타겟 본이 담당하는 소스 그룹의 마지막 본 월드 회전을 "가상 본 회전"으로 사용
- T-포즈 기준
offset = Inv(가상본 월드회전) * 타겟본 월드회전선계산 - 런타임:
타겟본.rotation = 가상본 월드회전 * offset
분배 대상 본(Spine/Chest/UpperChest/Neck)은 일반 SyncBoneRotations에서 제외된다.
소스 체인이 비어 있으면 자동 비활성화되고 일반 회전 복사로 폴백한다.
TwoBoneIKSolver — 자체 IK 솔버
FinalIK 등 외부 패키지 없이 직접 구현한 Two-Bone IK. 양팔/양다리 4개 사지에 적용.
LimbIK 구조:
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 값으로 손가락 포즈를 수동 제어.
작동 원리 (트릭):
SetHumanPose호출 전, 손가락 외 24개 본의 로컬 회전 저장HumanPoseHandler.SetHumanPose()로 손가락 머슬 적용 (전신에 영향)- 저장했던 비손가락 본 회전 즉시 복원 → 몸 포즈/본 길이 변형 방지
Muscle 인덱스 베이스: 왼손 muscles[55], 오른손
각 손가락 4개 머슬(curl 3 + spread 1) 구조.muscles[75]
제어 파라미터 (모두 -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:
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-포즈 기준 회전 차이 선계산
boneRotationDifferences[t, i] = Quaternion.Inverse(source.rotation) * target.rotation
런타임 (LateUpdate):
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 순서 (코루틴):
ResetScale()+ avatarScale=1- 1프레임 대기
- 목 높이 비율(
sourceNeck.y / targetNeck.y, 0.1~3 클램프)로 avatarScale 계산 - 1프레임 대기
CalibrateHeadToForward()+ 저장 (힙 높이는 런타임 자동 보정)
주의: private 필드 접근에 Reflection(BindingFlags.NonPublic)을 사용한다
(avatarScale, fingerCopyMode 등). 필드명을 바꾸면 원격 제어가
무음 실패하므로 GetPrivateField/SetPrivateField의 경고 로그를 확인할 것.
열거형
// 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= 리타게팅된 메인 아바타 Animatortargets[]= 동기화할 아바타 + 옵션(hipOffset, syncRootScale)
자주 발생하는 문제
| 증상 | 원인 | 해결 |
|---|---|---|
| 아바타가 전혀 안 움직임 | optitrackSource 미연결 / 스켈레톤 미감지 |
StreamingClient IP·본 매핑 확인 |
IK 루트를 찾을 수 없습니다 에러 |
소스 하위 IK 오브젝트/하위 본 누락 |
소스 프리펩의 IK 계층 확인 |
| 힙이 떠있음/파묻힘 | 다리 길이 자동 보정 오작동 (소스/타겟 다리 본 누락) | 다리 본 매핑 확인, floorHeight로 미세 조정 |
| 무릎/팔꿈치 방향 이상 | 다리는 소스 IK 본, 팔은 bendGoal 기준 | 소스 IK 본 위치 확인 (무릎 수동 조정은 제거됨) |
| 손가락이 이상하게 꺾임 | 머슬 인덱스/활성화 문제 | fingerShaped.enabled·손별 enable 확인 |
| 발이 바닥을 뚫거나 뜸 | floorHeight / 발 높이 가중치 |
floorHeight, footHeight*Threshold 조정 |
| 멀티 아바타 포즈 틀어짐 | SimplePoseTransfer Init 타이밍 |
Play 후 Init() 재호출 / 실행 순서 확인 |
| 원격 파라미터 변경 안 됨 | Reflection 필드명 불일치 | 콘솔의 필드를 찾을 수 없음 경고 확인 |