테니스 스윙 영상을 다른 3D 캐릭터에 옮기다 — 지터 잡으려고 며칠을 판 이야기
목표는 단순했다. 테니스 스윙 영상 한 편에서 사람 동작을 뽑아서, 전혀 다른 3D 캐릭터(믹사모)에 그대로 입히기. GUI로 손으로 만지는 게 아니라 영상 → 모션 추정 → 리타겟팅 → 렌더까지 전부 스크립트로 끝까지 이어지는 걸 증명하는 게 이번 POC의 핵심이었다.
VFX 쪽에서 리타겟팅 자체는 익숙한 작업이다. 문제는 이번엔 상용 툴(모션빌더 같은) 없이 무료 오픈소스만으로, 그것도 GUI 조작 없이 헤드리스로 끝까지 가야 한다는 것. 결과부터 말하면 며칠에 걸쳐 겉보기엔 똑같이 “캐릭터가 뒤틀린다”로 보이는 서로 다른 원인 대여섯 개를 하나씩 벗겨내야 했다.
코드 원인 분석이랑 실제 수정·실행은 전부 클로드 코드랑 대화하면서 방향을 잡아갔다. 내 역할은 렌더링 결과나 스크린샷을 보고 “이 자세는 해부학적으로 이상하다” “이건 자연스럽다”를 판단하고 다음 방향을 제안하는 쪽이었고, 실제로 라이브러리 소스를 읽고 버그를 찾아 패치하고 대조 실험을 설계하는 건 클로드가 진행했다.
파이프라인 한눈에
1
2
영상(테니스 스윙) → GVHMR(Kaggle GPU, 영상→SMPL 3D 모션 파라미터)
→ Blender에서 리타겟 → Mixamo 캐릭터에 입히기 → 원본과 나란히 렌더 비교
SMPL은 사람 몸을 22개 관절 회전값으로 표현하는 표준 포맷이고, 마침 믹사모 캐릭터의 본 구조가 이 22관절과 부모-자식 관계까지 거의 1:1로 맞아떨어져서 리타겟 대상으로 골랐다.
1차 문제 — 캐릭터가 공중에 뜨고 팔다리가 이상하게 꺾임
SMPL 모션을 믹사모 FBX에 바로 입혀봤더니 캐릭터가 바닥에서 붕 뜨고 팔다리가 원본과 다른 방향으로 꺾였다. 클로드가 사용한 라이브러리(Motius) 소스를 직접 읽어서 원인을 찾았는데, 팔 체인(어깨~손목) 딱 6개 본에만 방향 보정이 걸려있고 척추·목·다리는 아예 보정 대상이 아니었다. 리깅 용어로 하면 “레스트 포즈 불일치 보정”이 팔에만 부분적으로 되어 있던 셈. 척추·다리 체인까지 보정 범위를 넓히는 패치로 해결.
2차 문제 — 목·어깨·팔이 한 프레임 만에 150도씩 튄다
접지 문제를 잡고 나니 이번엔 목·어깨·팔이 한 프레임 만에 150~180도씩 튀는 증상이 나왔다. 처음엔 흔한 짐벌락이라 생각했는데, 클로드가 파이프라인을 “영상→뼈대”, “SMPL 적용(numpy로 순수 계산)”, “FBX로 export→재import”, “리타겟팅 후” 4단계로 쪼개서 각 단계를 독립적으로 대조해봤더니 — Blender 안에서만 재생할 땐 전혀 안 튀는데, FBX로 한 번 구웠다 다시 불러오는 순간에만 폭발한다는 게 나왔다.
FBX 바이너리를 텍스트로 열어 직접 까본 결과, FBX 포맷 자체가 회전을 쿼터니언이 아니라 항상 오일러(XYZ) 3채널로만 저장한다는 걸 확인했다. Blender 안에서는 쿼터니언이라 안전했는데, FBX로 굽는 순간 매 프레임을 독립적으로 오일러로 변환하면서 실제 짐벌락이 새로 생기는 구조였다. 이전 프레임과 이어지는 연속적인 오일러로 변환하도록 고쳐서 해결했다.
그래도 남은 잔여 떨림, 후처리로는 안 잡혔다
150도짜리 큰 스파이크는 없앴지만 19~60도 수준의 잔여 떨림이 남았다. 이걸 후처리(이상치 프레임을 보간으로 매끄럽게)로 더 다듬으려는 시도를 여러 번 했는데, 매번 “메모리상으로는 됐다”가 실제로 파일로 저장하고 다시 열면 도로 나빠졌다. 몇 번을 반복해도 같은 패턴이라 원인을 못 찾고 이 경로는 접었다.
방향을 완전히 바꿨다 — 후처리 대신 처음부터 다시 만들기
여기가 이번 작업의 진짜 전환점이었다. 후처리로 다듬는 대신, 1단계(SMPL 모션을 뼈대에 입히는 단계) 자체를 스무딩 없이 순수하게 다시 구현하기로 했다. 그런데 이 재구현 과정에서 겉보기엔 똑같이 “캐릭터가 실타래처럼 붕괴한다”로 보이는 원인을 세 개나 더 만났다.
- 레스트 포즈 합성을 빼먹음 — 본의 “쉬는 자세” 기준을 안 곱하고 회전값을 그냥 대입해서 캐릭터가 튜브처럼 무너짐. 흔한 리깅 실수인데, 합성 공식을 복원해서 해결.
- 좌표축이 안 맞음 — 원본 SMPL 데이터가 Y축이 위인 좌표계인데 Blender 쪽은 Z축이 위인 좌표계라, 축 변환을 빼먹고 있었다.
- 진짜 찾기 어려웠던 것: Blender 내부 상태가 실시간으로 안 갱신됨 — 부모 본을 코드로 먼저 움직이고 바로 다음 줄에서 자식 본을 움직이면, Blender가 “부모가 방금 움직였다”는 걸 즉시 반영을 안 해줘서 자식이 이사 가기 전 옛날 주소로 부모를 찾아가는 일이 생겼다. 클로드가 “회전을 아예 0으로 놓아보자”는 대조 실험을 설계해서 좌표축 문제와 이 버그를 분리해냈고, 본 하나를 움직일 때마다 강제로 상태를 갱신시키는 한 줄로 해결됐다.
이 세 개는 증상이 완전히 똑같아서(다 “몸이 뒤틀린다”) 눈으로만 봐서는 구분이 안 됐다. 매번 “정말 그런지 대조군을 만들어서 확인”하는 방식으로 하나씩 지워나간 게 결정적이었다.
믹사모 캐릭터에 직접 입히기 — 성공
재구현한 깨끗한 모션을 실제로 스키닝된 믹사모 메쉬(후드·바지·신발·머리카락 까지 다 붙어있는 캐릭터)에 직접 입혀봤다. 여기서도 캐릭터 리그 자체에 걸려있던 좌표 보정(90도 회전 + 스케일) 때문에 두 번 더 헤맸지만, 최종적으로 정면·측면 전 구간에서 해부학적으로 자연스러운 자세, 원본 스윙 동작을 그대로 따라가는 결과를 얻었다.
원본 영상과 결과물을 나란히 붙인 비교 영상: mixamo_fk_clean_comparison.mp4
언리얼로도 시도해봤는데
같은 재구현 데이터를 Unreal Engine의 리타겟 도구에도 연결해봤다. 그런데 예전에 겪었던 것과 똑같은 “허리가 뒤틀려 보인다”는 증상이 다시 나왔다.
“블렌더랑 언리얼이 좌표축 자체가 다른 거 아니냐”는 가설을 세우고, 클로드가 레스트 포즈 좌표를 양쪽에서 직접 찍어서 밀리미터 단위로 대조까지 해봤는데 — 완벽하게 일치했다. 축 문제가 아니었다는 게 실측으로 반박된 것. 진짜 원인은 리타겟된 골반 본의 회전이 전 구간에서 아예 얼어붙어 움직이지 않는 것으로 좁혀졌다(리타겟 도구의 특정 설정 쪽 문제로 추정). 여기는 아직 미해결이라 다음에 이어서 파볼 예정.
느낀 점
- 결과가 이상하게 나올 때 “대충 이런 이유겠지”로 넘기지 않고, 매번 대조군을 만들어서 원인을 하나씩 지워나가는 방식이 이번에 제일 크게 도움이 됐다. 증상이 똑같아 보여도 원인은 서로 다를 수 있다는 걸 여러 번 확인했다.
- 리타겟팅에서 흔히 겪는 리깅 문제(레스트 포즈 불일치, 트위스트 미보정)와, 순수한 소프트웨어 버그(내부 상태 갱신 타이밍)가 겉으로는 완전히 같은 증상으로 나타날 수 있다는 게 흥미로웠다. VFX 경험만으로는 “뻔한 리깅 실수”라고 단정하기 쉬운데, 실제로는 전혀 다른 층위의 문제였던 경우가 섞여 있었다.
- 코드를 직접 짜지 않아도, 클로드와 계속 대화하면서 “지금 뭘 확인했고 뭐가 아직 불확실한지”를 계속 짚어가는 것만으로도 원인 추적이 꽤 체계적으로 진행됐다. 결과물을 눈으로 판단하는 건 내 몫, 그 판단을 근거로 다음에 뭘 확인할지 좁혀가는 건 같이 하는 식이었다.
산출물
- 저장소: motion-retarget-poc
- 전체 시행착오 상세 기록: RETARGETING_JITTER_INVESTIGATION.md
다음에 할 것
- Unreal 리타겟터의 골반 회전 문제 계속 파기
- 지금은 스크립트마다 경로가 하드코딩돼 있어서 다른 영상으로 바로 못 돌린다 — CLI 인자로 바꾸는 작업