본문 바로가기
Unreal_최적화/Unreal Convert Physics to BoneAnim

[Unreal_최적화] 피직스 / 카와이 시뮬 > 본애님 자동화_1

by 레몬소스 2026. 8. 5.

[Unreal_TA] 런타임 물리 -> 본애님 자동화 - 시작하는 이유 (1편)

 
결과 비교

베이킹된 본 애님
카오스 피직스 시뮬

 
 
위는 베이킹된 본 애니메이션 / 하단은 카오스 피직스 물리 입니다.

-  부하가 큰 피직스/클로스 연출이나 복잡한 본(Bone) 계산을 일반 애니메이션 시퀀스 파일(.uasset)로
구워내어 프레임을 방어하는 사기적 최적화 기법입니다.

복잡한 런타임 연산을 0으로 만들어, 모바일/콘솔 환경에서 대규모 연출을 프레임 드랍 없이 사용

-사용 예시
  1. 맵에 펄럭이는 망토/깃발을 수십 개 배치해야 하는데 프레임이 안 나올 때
  2. 카오스 피직스(Chaos)나 클로스(Cloth) 시뮬레이션을 모바일에서도 똑같이 보여줘야 할 때
  3. 외부 DCC 툴(Max/Maya/Blender)과 언리얼 간의 릭(Rig) 호환 문제로 키프레임이 깨질 때

---
개념 이해: 왜 '베이크(Bake)'를 해야 할까?

우리가 매일 쓰는 '라이트맵 베이크'가 복잡한 빛 연산을
텍스처 한 장으로 구워 메모리를 아끼는 것처럼,
애니메이션 베이크는 매 프레임 일어나는 복잡한 '물리
/본(Bone) 연산 결과물'을 하나의 움직임
데이터(시퀀스)로 구워버리는 작업**입니다.

쉽게 말해, 엔진이 실시간으로 "이 천이 바람에 어떻게 흔들리지?"라고 머리 싸매며 계산하게 두는 것이 아니라, 이미 계산 끝난 움직임이니까 넌 재생만 해!"
라곰완성된 비디오를 틀어주는 것과 같습니다.

국내 실무에서는 의외로 이 개념을 놓치고 실시간
시뮬레이션을 남발하다가 최적화 단계에서 고생
하는 경우가 많습니다.

---

실무 적용 3단계

실무 TA의 주의사항

애니이션을 베이크하면 런타임 연산(CPU/GPU)은 드라마틱하게 줄어들지만,

대신 애니메이션 파일 용량(메모리)이 늘어납니다.

프레임과 패키지 용량 사이의 트레이드 오프를 반드시 고려하세요!


시작하게 된 계기
MMO 특성상 다수의 캐릭터가 나올 수 있는 상황을 대비해, 서브본 물리를 가벼운 Kawaii Physics(카와이)를 쓰거나
본 애니메이션으로 처리하는 경우가 많음.
 그런데 프로젝트에서 퀄리티 이슈가 있었고 카와이를 사용해본 경험도 부족해,
카오스 피직스(리지드바디 시뮬레이션)를 사용하는 상황이었음.
 
최적화 부분에 이슈가 발생해 해당 카오스 피직스 솔버를 사용하지 않기로 결정했으나,
클라이언트 쪽에서 카와이 지원이 불가하다고 해서 본 애니메이션으로 전환하기로 했음.
 
본 애니메이션은 손으로 잡지 않고 스프링 컨트롤러를 적용한 뒤 키로 베이킹하는 방식인데,
해당 방식의 문제점은 본 세팅을 몇몇 틀로 규격화해야 해서 메시 셰이프의 다양성이 많이 사라지고,
본이 많아질수록 3ds Max에서 작업 난이도가 올라가는 이슈가 생긴다는 점이었음.
 
어차피 Max에서 스프링을 구울 거라면, 언리얼의 리지드바디를 구우면 된다고 생각해 툴을 개발함.
게임에서는 그냥 구워진 애님만 재생하면 되므로 시뮬 비용이 0이 됨.
 
 
이 툴이 하는 일

  • 의상별 지글본 흔들림을 개별적으로 베이크
  • 그 서브본 트랙만 뽑아서 마스터 애님(예: Run, Idle)에 병합
  • 런타임에는 물리 시뮬 없이, 이미 흔들리는 애님이 재생됨

즉 보이는 결과는 똑같은데 런타임 물리 비용만 사라짐. MMO에서 이게 핵심.
 
 
구조 설계
  
일단 맨 처음 어셋을 어떻게 분리할지에 대한 결정을 해야 했음.
 애님 10개, 메시 서브본 세트 기준 10종이라고 예를 들고, 메시의 공통 본 150개 + 서브본 20개로 이루어졌다고 가정했음.
 
 
1안 - 각 메시별(서브본별 애님 분리)
그럼 애님 파일의 개수가 서브본 세트 기준으로 늘어나게 되며, 메시에 따라 다른 애님을 틀어주면 됨.
메시나 애님 추가 시 기존 것을 건들지 않으므로, 한 번 픽스된 데이터는 더 이상 만지지 않아도 되어 오류도 없음.
최적화 이슈 또한 애니메이션당 트랙이 150개+20개 수준이라 문제 없음.
대신 애님과 메시를 매칭하는 개발이 필요하며, 자동화 시 네임 태그 등으로 맞춰줘야 해서 오류가 발생할 수 있음.
2안 - 하나의 애님에 모든 서브본 트랙을 추가
애님은 기존대로 모션당 1개 유지, 클라이언트 개발은 별도로 없음.
애님이 1개이므로(예: 의상 10종) 각 서브본 트랙을 다 추가하게 되면, 애니메이션 파일 자체의 트랙이 약 150 + 300인 상황이 됨.
파일이 1개이므로 별도의 에셋 추가 시 데이터 오염이나 업데이트 검증이 어려움.
현 팀의 기조가 최대한 복잡하게 갈아 끼우거나 이름 등으로 찾는 행위를 자제하고 있음(하드 레퍼런스와 데이터 에셋 위주로 개발 진행).
그래서 추가 개발이 없는 2안을 방향으로 잡고, 이게 가능한지 먼저 검증함.
 
 
검증할 부분
 
이 방식엔 함정이 하나 있음. 마스터 애님 하나에 모든 의상의 서브본을 다 심는다는 거. 의상이 50벌이면
그 애님 하나에 트랙이 수백 개가 붙음.
트랙 수백 개짜리 애님을 런타임에 재생하면, 안 쓰는 트랙까지 다 계산하느라 오히려 더 느려지거나
오버헤드가 발생하지 않을지 우려되어 해당 이슈에 대한 검증이 필수적으로 동반되어야 함
 
 
실제 에셋으로 테스트자체가 어려워 엔진 소스를 확인해 판단하기로함
 
 
결론부터 말하면 문제없음.
 
애님을 압축 해제해서 포즈로 만드는 지점에서,
그 메시의 RequiredBones에 없는 트랙은 처리 목록에 아예 안 들어감.
 
상세 찾는 과정
 
일단 당연히 애니매이션 플레이 노드를 우선 확인을 해야함
 
 

 
위 이름대로 검색하면 AnimNode_SequencePlayer.h / cpp 를 찾을수 있음
일단 헤더 함수 목록들을 살펴보면
 

 
플레이등의 이름의 함수는 없지만 중간쯤 Evaluate_AnyThread 라는 평가항목의 함수가 있다
그외에는 실제 플레이관련 함수라고 예상되는부분이 없었음
 
물론 이전에 애님인스턴스를 수정해본적이 있어서 쉽게 찾았지 그 경험이 없었다면 하나하나 코드를 봐야 했을것 같음.
이벨류에이션 함수를 살펴보면 떡하니 가운데
GetAnimationPose( 라고 현재 포즈를 평가하는 함수가 존재함

 
 
역시 쭉 따라가보면
이렇게 본의 포즈를 평가하는 함수가 존재하고

 
다 기술하기엔 내용이 많아 좀 스킵을 하지만
이런식으로 본인덱스자체를 갖고 있는 본만 처리하고 있음
결국 일단 없는 본인덱스 자체는 스킵하는 구조 조금 더 내려가보면
 

Engine/Source/Runtime/Engine/Private/Animation/AnimationDecompression.cpp

// Only try and evaluate compressed bone data if the animation contains any bone tracks

if (NumTracks != 0) {

    // Evaluate compressed bone data

    const EAnimInterpolationType InterpolationType =

        ExtractionContext.InterpolationOverride.Get(Interpolation);

FAnimSequenceDecompressionContext DecompContext(

        PlatformTargetFrameRate.Default,

        PlatformTargetFrameRate.Default.AsFrameTime(GetPlayLength()).RoundToFrame().Value,

        InterpolationType,

        GetRetargetTransformsSourceName(),

        *PlatformCompressedData.CompressedDataStructure,

        GetSkeleton()->GetRefLocalPoses(),

        PlatformCompressedData.CompressedTrackToSkeletonMapTable,

        GetSkeleton(),

        IsValidAdditive(),

        AdditiveAnimType

    );



UE::Anim::Decompression::DecompressPose(

        OutPose,

        PlatformCompressedData,

        ExtractionContext,

        DecompContext,

        GetRetargetTransforms(),

        RootMotionReset

    );

}

 
실제로 포즈를 받아올 수 있는 함수가 최종적으로 실행되고 있음
 
 
이 디컴프레스포즈의 함수 내용이 너무 길어서 다 기술할 순 없고
 
AnimationDecompression.cpp
 

DecompressPose()

..

..

..

SCOPE_CYCLE_COUNTER(STAT_BuildAnimTrackPairs);



// Handle root bone separately if it is track 0. so we start w/ Index 1.

for (int32 TrackIndex = (bFirstTrackIsRootBone ? 1 : 0); TrackIndex < NumTracks; TrackIndex++)

{

    const int32 SourceSkeletonBoneIndex = CompressedData.GetSkeletonIndexFromTrackIndex(TrackIndex);

    const int32 TargetSkeletonBoneIndex = bIsSkeletonRemappingValid ? SkeletonRemapping.GetTargetSkeletonBoneIndex(SourceSkeletonBoneIndex) : SourceSkeletonBoneIndex;



    if (TargetSkeletonBoneIndex != INDEX_NONE)<본 인덱스를 확인하는작업

    {

       const FSkeletonPoseBoneIndex SkeletonPoseBoneIndex(TargetSkeletonBoneIndex);

       const FCompactPoseBoneIndex BoneIndex = RequiredBones.GetCompactPoseIndexFromSkeletonPoseIndex(SkeletonPoseBoneIndex);

       //Nasty, we break our type safety, code in the lower levels should be adjusted for this

       const int32 CompactPoseBoneIndex = BoneIndex.GetInt();

       if (CompactPoseBoneIndex != INDEX_NONE)<- 역시 본 인덱스를 확인한뒤 있다면 실제 평가목록에 추가하고있다

       {

          RotationScalePairs.Add(BoneTrackPair(CompactPoseBoneIndex, TrackIndex));



          if (bIsMeshSpaceAdditive)

          {

             AnimatedCompactRotations[CompactPoseBoneIndex] = true;

          }

 
비슷한 구문이 애님포즈 평가에 반복되는데
 
핵심은 CompactPoseBoneIndex != INDEX_NONE 체크임. 흐름을 보면
 

  1. 애님의 트랙을 하나씩 순회하면서
  2. 그 트랙이 가리키는 본이 현재 메시의 RequiredBones에 있는지 확인함 (GetCompactPoseIndexFromSkeletonPoseIndex)
  3. 없으면 INDEX_NONE이 나오고, 디코드 대상 목록(RotationScalePairs / TranslationPairs)에 추가되지 않고 그냥 넘어감

 
실제 압축 해제는 이 목록에 담긴 트랙에 대해서만 일어남
 
 즉 A 의상을 입은 캐릭터는, 같은 애님에 B·C·D 의상의 서브본 트랙이 들어있어도
자기 본에 해당하는 트랙만 평가함(아예 목록제서 제외)
 
남는 비용은 에셋 파일 크기가 트랙 수만큼 조금 커지는 것 정도인데,
애님 데이터는 압축돼서 저장되고 안 쓰는 트랙은 런타임에 평가조차 안 되니 성능엔 사실상 영향 없음.
 
최종적으로 만들고 다시 한번 확인하겠지만 일단 코드상으로는 애님 트랙이 많아지는건
트랙자체가 많으니 루프문에 어느정도 오버헤드는 존재할거라고 보지만 실제 훨씬 더 많은 코드가 있는 현재 본평가는
아예 목록자체가 없음을 확인함
 
---
다음편
실제 만들기전에 검증해야할 에셋 목록들
- 실제 물리가 진짜 키로 구워지나
- 구워진다면 c++ / 블루프린트 중 어느형식으로 개발을 해야하나
자동화 굽기 기능개발