..

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본). 이를 가상 본 그룹핑으로 매핑한다.

원리:

  1. 초기화 시 소스 체인(GetSpineChainTransforms/GetNeckChainTransforms)을 타겟 본 수만큼 그룹으로 분할
  2. 각 타겟 본이 담당하는 소스 그룹의 마지막 본 월드 회전을 "가상 본 회전"으로 사용
  3. T-포즈 기준 offset = Inv(가상본 월드회전) * 타겟본 월드회전 선계산
  4. 런타임: 타겟본.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 값으로 손가락 포즈를 수동 제어.

작동 원리 (트릭):

  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:

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 순서 (코루틴):

  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의 경고 로그를 확인할 것.


열거형

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