371 lines
18 KiB (Stored with Git LFS)
Markdown
371 lines
18 KiB (Stored with Git LFS)
Markdown
# 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 필드명 불일치 | 콘솔의 `필드를 찾을 수 없음` 경고 확인 |
|