애니메이션 리타게팅 포즈 비교 시스템으로 어깨·손목 트위스트 왜곡 자동 감지하기
애니메이션 리타게팅 후 발생하는 어깨 들림과 손목 비틀림을 어떻게 잡아낼 수 있을까요? Unity C# 환경에서 본 로컬 회전, 스윙-트위스트 분해, 시간 필터를 결합해 왜곡을 자동으로 감지하고 검수 파이프라인을 구축하는 실무 가이드를 정리합니다.

핵심 요약
다양한 체형의 캐릭터에 여러 애니메이션을 리타게팅할 때 어깨가 솟아오르거나 손목이 비틀리는 왜곡이 생길 수 있습니다. 이를 눈으로만 검수하면 에셋 수가 많을수록 시간이 들기 때문에 자동 지표로 검수 대상을 좁힐 수 있습니다.
골든 포즈 애니메이션과 리타게팅된 관절의 로컬 회전(Local Rotation)을 비교하고 스윙-트위스트(Swing-Twist) 분해를 적용하면 기준에서 벗어난 회전과 특정 축의 비틀림 성분을 별도 지표로 계산할 수 있습니다. 이 값은 눈에 보이는 왜곡 자체를 증명하지 않으므로 연속 프레임 필터와 관절별 임계값을 적용한 뒤 아티스트가 확인할 후보를 추리는 데 사용합니다.
| 검수 단계 | 입력 데이터 | 출력 결과 | 주요 목적 |
|---|---|---|---|
| 1. 시간 샘플링 | 골든 애니메이션 & 리타게팅 캐릭터 | 동일 프레임(시간)별 본 로컬 회전값 | 시간 축 동기화 및 포즈 추출 |
| 2. 오차 계산 및 분해 | 기준 회전() vs 관측 회전() | 전체 회전 오차() & 트위스트 오차() | 축별 비틀림 및 스윙 왜곡 분리 |
| 3. 필터링 및 판정 | 관절별 임계값 & 연속 프레임 윈도우 | 경고 본, 심각도(Warning/Error), 문제 구간 | 일시적 튐 제거 및 아티스트 리뷰 큐 생성 |
애니메이션 리타게팅 후 어깨와 손목 왜곡은 왜 발생하는가?
리타게팅(Retargeting)은 서로 다른 스켈레톤 구조, 본 길이, 로컬 축 방향, 초기 바인드 포즈(T-Pose vs A-Pose)를 가진 리그 간에 모션을 매핑하는 기술입니다. 하지만 소스와 타깃 캐릭터의 골격 비율이나 롤(Roll) 축 설정이 조금만 달라도 모션 전달 과정에서 회전 분배가 왜곡되기 쉽습니다.
현업에서 가장 흔하게 마주치는 원인들을 정리하면 다음과 같습니다.
- 쇄골(Clavicle) 유무의 차이: 소스 리그에는 쇄골 모션이 포함되어 있지만 타깃 캐릭터에 쇄골 본이 없거나 매핑이 빠져 있다면 상완(UpperArm) 본 하나가 모든 회전을 흡수하면서 어깨가 과도하게 치솟게 됩니다.
- 로컬 롤(Roll) 축 불일치: 상완이나 전완(LowerArm)의 길이 방향 축(예: X축 vs Z축)이 꼬여 있으면 팔을 앞으로 뻗는 단순 굽힘 모션이 원치 않는 축의 트위스트 회전으로 누적됩니다.
- 전완 트위스트 분배 실패: 팔의 회내·회외는 주로 요골과 척골 사이 관절에서 일어나며, 캐릭터 리그는 이를 전완 트위스트 본 등으로 표현할 수 있습니다. 리그의 분배 설정이 맞지 않으면 전완 또는 손 본에 회전이 과도하게 몰려 비틀려 보일 수 있습니다.
- T-Pose와 A-Pose 간의 오프셋 누적: 기준 포즈가 완벽히 정렬되지 않은 상태에서 리타게팅 행렬을 곱하면 전 구간에 걸쳐 일정한 회전 오차가 베이크되어 버립니다.
- 본 길이 차이로 인한 위치 왜곡: 팔 길이가 서로 다른 캐릭터의 경우, 로컬 회전값 자체는 정상이어도 월드 좌표상 손끝 위치가 어긋나면서 IK나 리타게터가 무리하게 관절을 꺾어버릴 수 있습니다.
따라서 단순히 월드 위치(World Position)만 비교해서는 문제의 원인을 짚어내기 어렵습니다. 반드시 부모 기준의 로컬 회전(Local Rotation)을 1차 지표로 분석하고 엔드 이펙터(손, 팔꿈치) 위치를 보조 지표로 함께 교차 검증해야 합니다.
포즈 비교 시스템의 좌표계와 데이터 설계
정확한 검수를 위해서는 ‘언제’, ‘무엇’을 기준으로 비교할 것인지 데이터 파이프라인을 명확히 정의해야 합니다.
1. 비교 기준 포즈(Golden Reference) 정의
단순히 정적인 기본 포즈를 비교하는 것이 아니라 동작의 시간 흐름()에 맞춘 골든 애니메이션(Golden Animation) 데이터를 준비합니다.
- 아티스트가 최종 승인한 표준 캐릭터의 애니메이션 클립
- 리타게팅 이전 원본 소스 모션을 타깃 스켈레톤 좌표계로 정규화 변환한 데이터
- 품질이 보장된 샘플 데이터셋에서 추출한 관절별 허용 회전 범위(Angle Boundary)
flowchart LR
A["골든 레퍼런스 애니메이션"] --> C["동일 시간(t) 동기화 샘플링"]
B["리타게팅된 캐릭터 포즈"] --> C
C --> D["본별 로컬 회전(Local Rot) 오차 계산"]
D --> E["스윙-트위스트 (Swing-Twist) 축 분해"]
E --> F["시간 필터 & 관절별 임계값 판정"]
F --> G["에디터 히트맵 표시 & 로그 리포트"]
2. 관절별 핵심 수집 지표
비교 시스템에서 수집하는 데이터는 다음과 같이 계층화하여 관리합니다.
| 수집 지표 | 분석 목적 | 주요 검수 대상 관절 |
|---|---|---|
| 로컬 회전 오차 () | 기준 자세 대비 전체적인 회전 편차 측정 | Clavicle, UpperArm, LowerArm, Hand |
| 트위스트 오차 () | 본 길이 축을 중심으로 비틀린 각도 측정 | UpperArm(어깨 롤), LowerArm, Hand |
| 엔드 이펙터 거리 오차 () | 손목/손끝의 루트 기준 상대 위치 오차 확인 | Wrist, Hand, Elbow |
| 각속도 변화량 () | 프레임 간 회전이 튀는 현상(Flip/Spike) 감지 | 모든 상체 관절 |
본 회전 오차와 트위스트 오차의 수학적 계산법
두 포즈 사이의 차이를 구하기 위해 기준 회전 쿼터니언을 , 리타게팅 결과의 관측 회전 쿼터니언을 라고 정의하겠습니다.
1. 상대 회전(Delta Rotation)과 전체 각도 오차
기준 상태에서 관측 상태로 가기 위한 상대 회전 행렬(또는 쿼터니언) 은 다음과 같이 구합니다.
두 회전의 사잇각(Angular Error)은 쿼터니언 내적(Dot Product)의 성질을 이용하여 계산할 수 있습니다. 쿼터니언에서 와 는 물리적으로 동일한 회전을 나타내므로 반드시 내적값에 절댓값을 씌워야 합니다.
2. 스윙-트위스트 분해(Swing-Twist Decomposition)
전체 각도 오차 만으로는 이 오차가 어깨가 위로 들린 것(Swing)인지 아니면 팔이 안쪽으로 꼬인 것(Twist)인지 구분할 수 없습니다. 따라서 상대 회전 을 본의 길이 축(Twist Axis)에 대한 회전과 그 외 축에 대한 회전으로 분리해야 합니다.
본의 로컬 트위스트 단위 축을 라 하고 상대 회전 쿼터니언을 벡터 성분 와 스칼라 성분 로 표현()할 때, 축 로의 벡터 투영(Projection)을 통해 트위스트 쿼터니언을 추출합니다.
이후 트위스트 회전각의 부호(Signed Angle)를 포함하여 오차를 계산합니다.
[!IMPORTANT]
트위스트 축 는 반드시 상대 회전 쿼터니언과 동일한 로컬 좌표계에 정의되어 있어야 합니다. Unity의Transform.localRotation을 기준으로 계산한다면 임포트된 리그에서 해당 본의 축을 확인해 프로필에 지정해야 합니다.Vector3.right나Vector3.forward가 모든 휴머노이드 리그에 공통으로 맞는 축은 아닙니다.
Unity C# 기반 포즈 오차 측정 구현
Unity에서 사용할 수 있는 포즈 오차 계산 유틸리티 클래스와 데이터 구조의 예입니다. 임계값과 축은 실제 리그에서 검증해야 하며, 일부 코드는 계산 흐름을 보여 주는 예시입니다.
1. 포즈 오차 계산 유틸리티 (PoseError.cs)
using UnityEngine;
public static class PoseError
{
/// <summary>
/// 두 로컬 쿼터니언 간의 전체 회전 각도 오차를 계산합니다 (0 ~ 180도).
/// </summary>
public static float RotationErrorDegrees(Quaternion referenceLocal, Quaternion observedLocal)
{
// 동일 회전의 음수 표현(q == -q)을 고려해 절댓값을 취합니다.
float dot = Mathf.Clamp(Mathf.Abs(Quaternion.Dot(referenceLocal, observedLocal)), 0f, 1f);
return 2f * Mathf.Acos(dot) * Mathf.Rad2Deg;
}
/// <summary>
/// 지정된 로컬 트위스트 축을 기준으로 상대 회전의 비틀림 각도를 계산합니다 (-180 ~ 180도).
/// </summary>
public static float SignedTwistError(Quaternion referenceLocal, Quaternion observedLocal, Vector3 twistAxis)
{
Vector3 axis = twistAxis.normalized;
// 기준 회전 대비 관측 회전의 상대 델타 쿼터니언 계산
Quaternion delta = Quaternion.Inverse(referenceLocal) * observedLocal;
delta = Quaternion.Normalize(delta);
// 쿼터니언의 벡터 성분을 트위스트 축에 투영
Vector3 vectorPart = new Vector3(delta.x, delta.y, delta.z);
Vector3 projected = Vector3.Project(vectorPart, axis);
// 투영된 벡터와 원래 스칼라 성분으로 트위스트 쿼터니언 재구성
Quaternion twist = new Quaternion(projected.x, projected.y, projected.z, delta.w);
twist = Quaternion.Normalize(twist);
Vector3 twistVector = new Vector3(twist.x, twist.y, twist.z);
float angle = 2f * Mathf.Atan2(Vector3.Dot(twistVector, axis), twist.w) * Mathf.Rad2Deg;
// -180 ~ 180도 범위로 래핑
return Mathf.DeltaAngle(0f, angle);
}
}
2. 리그 규칙 및 검사 데이터 정의 (BonePoseRule.cs)
using System;
using UnityEngine;
[Serializable]
public sealed class BonePoseRule
{
public HumanBodyBones bone = HumanBodyBones.RightUpperArm;
[Tooltip("본의 길이 방향 로컬 트위스트 축")]
public Vector3 twistAxis = Vector3.right;
[Tooltip("전체 회전 허용 한계각 (Degree)")]
public float maxRotationError = 20f;
[Tooltip("트위스트 회전 허용 한계각 (Degree)")]
public float maxTwistError = 25f;
[Tooltip("전체 점수 계산 시 가중치")]
public float weight = 1f;
}
3. 프레임별 오차 평가 로직
using UnityEngine;
public static class PoseFrameEvaluator
{
public static float EvaluateFrameViolation(BonePoseRule rule, Quaternion refLocal, Quaternion obsLocal)
{
if (rule == null || rule.maxRotationError <= 0f || rule.maxTwistError <= 0f)
throw new System.ArgumentException("검사 규칙과 회전 임계값은 유효해야 합니다.", nameof(rule));
// 1. 전체 회전 오차 계산
float rotError = PoseError.RotationErrorDegrees(refLocal, obsLocal);
// 2. 특정 축 트위스트 오차 계산
float twistError = Mathf.Abs(PoseError.SignedTwistError(refLocal, obsLocal, rule.twistAxis));
// 3. 임계값 대비 초과율 계산
float rotRatio = rotError / rule.maxRotationError;
float twistRatio = twistError / rule.maxTwistError;
float maxRatio = Mathf.Max(rotRatio, twistRatio);
// 임계값(1.0)을 초과한 비율에 가중치를 곱해 위반 점수 누적
return Mathf.Max(0f, maxRatio - 1f) * rule.weight;
}
}
어깨 왜곡: 쇄골-상완-전완 체인 검사법
어깨 관절은 단독 본(UpperArm)만 봐서는 원인을 잡기 어렵습니다. 인체 해부학적으로 어깨의 움직임은 쇄골(Clavicle)의 거상(Elevation)/하강(Depression)과 상완골의 회전이 복합적으로 맞물려 일어납니다.
[Spine / Chest] ───> [Clavicle (쇄골)] ───> [UpperArm (상완)] ───> [LowerArm (전완)]
효과적인 어깨 검수 전략
- 쇄골-상완 상대 각도 분석:
쇄골이 위로 들리지 않은 상태에서 상완만 높게 올라가거나 반대로 쇄골만 과도하게 솟아 목을 파고드는 현상을 감지합니다. 쇄골의 로컬 Pitch 회전량이 기준치보다 높다면 쇄골 타깃 매핑 누락을 의심해야 합니다. - 스윙과 트위스트의 분리:
팔을 옆으로 들어 올리는 모션(T-Pose 방향)에서 상완의 트위스트 성분이 급격히 튀는지 검사합니다. 스윙은 정상인데 트위스트가 30도 이상 꺾인다면 리타게터의 본 롤 매핑이 잘못된 것입니다. - 손 위치와의 상관관계 체크:
어깨 체인의 각도 오차는 크지만 손끝의 상대 위치는 정상인 경우가 있습니다. 이는 캐릭터 간의 팔 길이 비율 차이로 인해 발생한 자연스러운 보정일 수 있으므로 단순 에러가 아닌 ‘비율 보정 경고’ 수준으로 분류하여 불필요한 수정을 줄입니다.

손목 왜곡: 전완과 손 본의 상호 연계 검사
손목 비틀림(Wrist Twist)은 리타게팅 파이프라인에서 가장 악명 높은 아티팩트 중 하나입니다.
전완-손목 트위스트 복합 지표 ()
전완(LowerArm)과 손(Hand)의 트위스트를 개별적으로만 보면 한쪽이 과도하게 꺾였는지 알아채기 어렵습니다. 다음과 같이 두 본의 트위스트 오차를 결합한 복합 지표를 활용합니다.
여기서 는 손 본의 가중치 계수(보통 0.5 ~ 0.8)입니다. 손 본은 무기 파지나 제스처로 인해 로컬 회전이 의도적으로 크게 변할 수 있으므로 전완의 트위스트 오차에 더 높은 가중치를 둡니다.
프레임 간 각속도 변화율(Flip Spike) 탐지
트위스트 각도 자체는 허용 범위 안에 있어도, 각도를 -180°~180°로 래핑하면 경계 부근에서 프레임 간 값이 크게 점프할 수 있습니다. 쿼터니언 q와 -q는 같은 회전을 나타내므로 부호가 바뀐 표현 자체를 관절 플립으로 판정하지 말고, 기준 축에 대한 연속 각도를 언래핑해 실제 변화와 구분해야 합니다.
큰 는 실제 급격한 회전일 수도 있고 각도 래핑으로 생긴 불연속일 수도 있습니다. 초당 360°는 프로젝트에서 검증할 예시 임계값일 뿐이며, 스파이크만으로 짐벌락이나 보간 오류라고 단정하지 말고 연속 각도와 원본 포즈를 함께 확인합니다.
실무 임계값(Threshold)과 심각도 점수화 설계
프로젝트의 그래픽 스타일과 캐릭터 체형에 따라 허용 오차는 달라집니다. 아래 숫자는 측정된 업계 표준이 아니라 프로덕션 파이프라인에서 시험할 수 있는 예시 시작값이며, 승인된 샘플 포즈와 아티스트 검수 결과로 조정해야 합니다.
| 검사 항목 | 권장 초기 임계값 | 완화/조정 기준 및 실무 팁 |
|---|---|---|
| 쇄골(Clavicle) 전체 회전 오차 | 15° ~ 25° | 격투나 투구 등 팔을 크게 휘두르는 모션은 최대 35°까지 완화 |
| 상완(UpperArm) 트위스트 오차 | 20° ~ 30° | 어깨 롤 보정 리그가 적용된 캐릭터는 15° 이내로 엄격하게 관리 |
| 전완(LowerArm) 트위스트 오차 | 20° ~ 25° | 손목 트위스트 본(Twist Bone)이 별도로 존재하는 경우 기준 완화 |
| 손(Hand) 루트 기준 위치 오차 | 캐릭터 신장의 1.5% ~ 3% | 본 길이 차이를 감안해 월드 좌표 대신 Pelvis/Root 기준 로컬 좌표 사용 |
| 연속 초과 프레임 윈도우 | 3 ~ 5 프레임 (at 30fps) | 단일 프레임 샘플링 노이즈로 인한 오탐 방지용 최소 지속 시간 |
종합 심각도 점수(Severity Score) 산출식
검수 툴이 수만 프레임의 데이터를 쏟아내면 아티스트가 우선순위를 정하기 어렵습니다. 따라서 관절별 위반 정도를 하나의 점수로 종합합니다.
- : 관측된 오차 ( 또는 )
- : 해당 본의 허용 임계값(Threshold)
- : 본의 시각적 중요도 가중치 (쇄골/전완: 1.5, 손가락: 0.3)
점수 가 0이면 정상(Pass), 0 < < 1.0이면 경미한 왜곡(Warning), ≥ 1.0이면 수정이 필요한 심각한 왜곡(Error)으로 분류합니다.
Step-by-Step: 포즈 비교 자동 검수 파이프라인 구축 가이드
실제 프로덕션 환경에 자동 포즈 비교 시스템을 도입하는 5단계 워크플로입니다.
1단계: 리그별 본 매핑 및 축 프로필(Rig Profile) 생성
캐릭터마다 본의 롤 축이 제각각인 경우가 많습니다. ScriptableObject 형태의 Rig Profile을 만들어 각 캐릭터 모델에 맞는 본 매핑과 로컬 트위스트 축을 미리 등록합니다.
2단계: 시간 기반 정규화 샘플링
프레임 레이트(30fps, 60fps)가 다른 클립들을 비교할 때는 프레임 인덱스 대신 정규화된 시간() 또는 절대 초(Second) 단위로 보간 샘플링합니다.
- 샘플링 시점마다 본별
localRotation과 주요 관절의root-relative position을 캡처하여 버퍼에 저장합니다.
3단계: 로컬 회전 및 트위스트 오차 일괄 연산
준비된 기준 데이터와 리타게팅 데이터를 프레임 단위로 순회하며 상대 쿼터니언을 구하고 스윙-트위스트 분해를 통해 와 를 계산합니다.
4단계: 시간 윈도우 필터 적용 (노이즈 억제)
단 1프레임 튀는 값에 바로 에러를 띄우면 오탐(False Positive)이 급증합니다.
- 연속 초과 판정: 설정한 임계값을 3~5프레임 이상 연속으로 초과할 때만 유효한 결함으로 등록합니다.
- 지수 이동 평균(EMA): 실시간 뷰어에서는 오차 그래프를 부드럽게 만들기 위해 EMA()를 적용합니다.
5단계: 아티스트 친화적 UI 및 리뷰 큐 출력
수치 나열 대신 아티스트가 즉각 조치할 수 있는 형태로 피드백을 시각화합니다.
- 씬 뷰 기즈모 히트맵: 왜곡이 심한 본을 초록색(정상) 노란색(주의) 빨간색(심각)으로 하이라이트 표시
- 타임라인 마커: 애니메이션 타임라인 바 위에 문제가 발생한 구간(Time Range)을 붉은색 바(Bar)로 표시
- 원인 추천 가이드: “RightLowerArm 트위스트 과다 리타게터의 Forearm Roll 축 매핑을 확인하세요”와 같은 액션 가이드 제공
오탐(False Positive)을 줄이기 위한 실무 체크리스트
자동 검수 시스템의 성패는 “얼마나 오탐 없이 정확한 에러만 골라내느냐”에 달려 있습니다. 다음 항목들을 사전에 점검해보세요.
- 루트 오리엔테이션 일치 여부: 기준 모델과 타깃 모델의 루트(Root/Hips) 초기 정면 방향이 일치하는지 확인했는가?
- 미러(Mirror) 애니메이션 처리: 좌우 반전 모션의 경우 트위스트 회전 부호가 반전되므로 미러 플래그에 따른 부호 보정을 적용했는가?
- 쿼터니언 절댓값 적용:
Quaternion.Dot계산 시 반드시Mathf.Abs를 취해 부호 반전으로 인한 360도 오차 계산을 방지했는가? - 트위스트 축 정규화: 투영 계산 전 트위스트 축 벡터가 반드시 단위 벡터(
normalized)인지 확인했는가? - 쇄골 누락 리그 예외 처리: 쇄골 본이 없는 단순화 리그의 경우, 상완의 허용 임계값을 자동으로 1.5배 상향 조정하도록 예외 규칙을 두었는가?
- 무기 및 프롭 장착 고려: 양손 무기를 쥐는 클립은 손목 회전보다 무기 소켓(Socket) 간의 거리와 방향 유지를 우선 검수하는가?
- 레이어 블렌딩 구간 검수: 단일 원본 클립뿐만 아니라 인게임에서 상/하체 분리 블렌딩이 일어난 최종 상태를 검사하고 있는가?
자주 묻는 질문 (FAQ)
Q. 월드 좌표계(World Position/Rotation)만으로 비교하면 안 되나요?
A. 권장하지 않습니다. 월드 좌표는 루트 모션의 이동량, 골반 높이, 각 본의 물리적 길이에 따라 완전히 달라집니다. 본의 길이가 긴 캐릭터는 완벽하게 리타게팅되었더라도 손의 월드 위치가 기준 캐릭터와 다를 수밖에 없습니다. 따라서 관절 자체의 각도 변형을 감지하는 로컬 회전이 핵심 기준이 되어야 하며 월드/루트 위치는 보조적인 검증 지표로만 활용해야 합니다.
Q. 본의 로컬 트위스트 축(Twist Axis)은 어떻게 찾아야 하나요?
A. 3D DCC 툴(Maya, 3ds Max, Blender)이나 Unity 씬 뷰에서 해당 본의 피벗 기즈모를 ‘Local’ 모드로 켜고 본의 자식 방향(뼈의 길이 방향)을 가리키는 축 화살표를 확인하면 됩니다. 3ds Max 기반 Biped는 주로 X축, Maya 휴먼IK는 X축 또는 Y축을 사용하는 경우가 많으므로 리그 생성 규칙에 맞춰 프로필로 저장해 두는 것이 좋습니다.
Q. 임계값을 초과한 클립은 무조건 실패(Fail) 처리해야 하나요?
A. 아닙니다. 연속 프레임 지속 시간과 게임 내 카메라 시점을 함께 고려해야 합니다. 예를 들어 1~2프레임 스파이크이거나 탑다운 뷰 게임에서 카메라와 먼 쪽 팔의 미세한 트위스트는 플레이어가 인지하기 어렵습니다. 따라서 시스템에서는 ‘리뷰 필요(Warning)‘로 큐에 쌓아두고 종합 점수가 높은 핵심 컷씬이나 메인 캐릭터 모션을 우선 검수하도록 운영하는 것이 효율적입니다.
마무리
애니메이션 리타게팅에서 발생하는 어깨 들림과 손목 비틀림은 눈으로 찾으려면 많은 시간과 피로가 따르지만 본 로컬 회전 기반의 스윙-트위스트 분해 기법을 적용하면 수학적으로 명확하게 잡아낼 수 있습니다.
리그별 축 프로필 표준화, 연속 프레임 필터링, 타임라인 히트맵 시각화를 함께 적용하면 수치 계산을 넘어 문제 구간을 확인하기 쉬워집니다. 대규모 캐릭터와 수천 개의 모션 에셋을 다루는 프로젝트에서도 리타게팅 품질을 안정적으로 유지하고 QA와 테크니컬 아트의 검수 시간을 줄일 수 있습니다.



