Skeletal Mesh 아키텍처
USkeleton, USkeletalMesh, FReferenceSkeleton의 내부 구조와 LOD 시스템까지, 스켈레탈 메시의 핵심 아키텍처를 심층 분석합니다.
USkeleton과 USkeletalMesh의 관계
UE5 애니메이션 시스템의 두 핵심 애셋인 USkeleton과 USkeletalMesh의 구조와 역할
UE5의 스켈레탈 메시 시스템은 크게 두 가지 핵심 UObject로 구성됩니다. USkeleton은 본(Bone) 계층 구조와 애니메이션 데이터를 관리하는 공유 애셋이며, USkeletalMesh는 실제 메시 지오메트리와 스키닝 정보를 담고 있는 렌더링 애셋입니다.
하나의 USkeleton은 여러 USkeletalMesh가 공유할 수 있습니다. 예를 들어 같은 캐릭터의 의상 변형(바디, 헤어, 장비)이 동일한 USkeleton을 참조하면, 모든 변형에 동일한 애니메이션을 적용할 수 있습니다. 이것이 UE5 모듈러 캐릭터 시스템의 기반입니다.
// USkeleton에서 FReferenceSkeleton 접근
const FReferenceSkeleton& RefSkel = Skeleton->GetReferenceSkeleton();
// 본 개수 조회
int32 NumBones = RefSkel.GetNum();
// 특정 본 인덱스 찾기
int32 BoneIndex = RefSkel.FindBoneIndex(FName("spine_03"));
// 레퍼런스 포즈에서 본 트랜스폼 가져오기
const FTransform& BoneRefTransform = RefSkel.GetRefBonePose()[BoneIndex];
FReferenceSkeleton 내부 구조
본 계층을 정의하는 FReferenceSkeleton의 데이터 레이아웃과 핵심 메서드
FReferenceSkeleton은 USkeleton 내부에서 본 계층 구조를 실제로 저장하는 구조체입니다. 모든 본의 이름, 부모-자식 관계, 레퍼런스 포즈(바인드 포즈) 트랜스폼을 관리합니다.
| 멤버 | 타입 | 설명 |
|---|---|---|
FinalRefBoneInfo |
TArray<FMeshBoneInfo> |
본 이름(FName)과 부모 본 인덱스를 저장 |
FinalRefBonePose |
TArray<FTransform> |
각 본의 로컬 스페이스 레퍼런스 트랜스폼 |
FinalNameToIndexMap |
TMap<FName, int32> |
본 이름 → 인덱스 빠른 검색용 맵 |
RequiredBones |
TArray<FBoneIndexType> |
LOD별 필수 본 인덱스 배열 |
FReferenceSkeleton은 순수한 데이터 구조체(struct)로, 본 트리의 정적인 정의만 담당합니다. 반면 USkeleton은 UObject로서 애니메이션 커브 매핑, Virtual Bone, Notify 이름 등의 메타데이터까지 관리합니다. 런타임에 본 트랜스폼을 수정하려면 FReferenceSkeleton이 아닌 FBoneContainer나 FCompactPose를 사용해야 합니다.
// FReferenceSkeleton에서 부모-자식 관계 탐색
void TraverseBoneHierarchy(const FReferenceSkeleton& RefSkel)
{
for (int32 i = 0; i < RefSkel.GetNum(); ++i)
{
const FMeshBoneInfo& BoneInfo = RefSkel.GetRefBoneInfo()[i];
FString ParentName = (BoneInfo.ParentIndex != INDEX_NONE)
? RefSkel.GetBoneName(BoneInfo.ParentIndex).ToString()
: TEXT("ROOT");
UE_LOG(LogAnimation, Log, TEXT("Bone[%d]: %s (Parent: %s)"),
i, *BoneInfo.Name.ToString(), *ParentName);
}
}
// 컴포넌트 스페이스 트랜스폼 계산
FTransform GetComponentSpaceTransform(const FReferenceSkeleton& RefSkel, int32 BoneIndex)
{
FTransform Result = RefSkel.GetRefBonePose()[BoneIndex];
int32 ParentIndex = RefSkel.GetRefBoneInfo()[BoneIndex].ParentIndex;
while (ParentIndex != INDEX_NONE)
{
Result = Result * RefSkel.GetRefBonePose()[ParentIndex];
ParentIndex = RefSkel.GetRefBoneInfo()[ParentIndex].ParentIndex;
}
return Result;
}
USkeletalMesh 렌더 데이터와 LOD
FSkeletalMeshRenderData 구조와 LOD 시스템의 동작 원리
USkeletalMesh의 실질적인 렌더링 데이터는 FSkeletalMeshRenderData에 저장됩니다. 이 구조체는 LOD(Level of Detail)별로 분리된 버텍스 버퍼, 인덱스 버퍼, 스키닝 웨이트 정보를 관리합니다.
LOD 전환 설정
FSkeletalMeshLODInfo에서 LOD 전환 거리, 화면 크기 비율, Hysteresis(히스테리시스) 값을 지정합니다. ScreenSize 기반 자동 전환이 기본이며, 수동으로 ForcedLodModel을 설정할 수도 있습니다.
본 제거(Bone Removal)
낮은 LOD에서는 불필요한 본을 제거하여 스키닝 연산을 절약합니다. BonesToRemove 배열로 LOD별 제거 본을 지정하면, 해당 본의 웨이트는 부모 본으로 자동 리매핑됩니다.
스킨 웨이트 프로파일
UE5에서는 하나의 메시에 여러 Skin Weight Profile을 저장할 수 있습니다. 런타임에 프로파일을 전환하면 동일 메시의 디포메이션 품질을 상황에 따라 조절할 수 있습니다.
스트리밍 LOD
USkeletalMesh는 LOD 데이터 스트리밍을 지원합니다. bSupportLODStreaming이 활성화되면 높은 LOD의 버텍스/인덱스 데이터를 필요할 때만 메모리에 로드하여 VRAM을 절약합니다.
// 스켈레탈 메시 LOD 정보 조회
void PrintLODInfo(const USkeletalMesh* SkelMesh)
{
const FSkeletalMeshRenderData* RenderData = SkelMesh->GetResourceForRendering();
for (int32 LODIdx = 0; LODIdx < RenderData->LODRenderData.Num(); ++LODIdx)
{
const FSkeletalMeshLODRenderData& LODData = RenderData->LODRenderData[LODIdx];
UE_LOG(LogAnimation, Log, TEXT("LOD %d:"), LODIdx);
UE_LOG(LogAnimation, Log, TEXT(" Vertices: %d"), LODData.GetNumVertices());
UE_LOG(LogAnimation, Log, TEXT(" Triangles: %d"), LODData.GetTotalFaces());
UE_LOG(LogAnimation, Log, TEXT(" Sections: %d"), LODData.RenderSections.Num());
UE_LOG(LogAnimation, Log, TEXT(" Active Bones: %d"), LODData.ActiveBoneIndices.Num());
UE_LOG(LogAnimation, Log, TEXT(" Required Bones: %d"), LODData.RequiredBones.Num());
}
}
USkeletalMeshComponent와 런타임 평가
스켈레탈 메시가 런타임에 평가되고 렌더링되는 전체 파이프라인
USkeletalMeshComponent는 USkeletalMesh 애셋을 월드에 배치하고 애니메이션을 적용하는 컴포넌트입니다. 매 프레임 애니메이션 평가, 본 트랜스폼 계산, GPU 스키닝을 거쳐 최종 렌더링됩니다.
| 단계 | 스레드 | 핵심 동작 |
|---|---|---|
| TickComponent | Game Thread | TickAnimation 호출, AnimInstance::UpdateAnimation 트리거 |
| ParallelAnimEvaluation | Worker Thread | AnimGraph 노드 트리 평가, FCompactPose 결과 계산 |
| CompleteParallelAnimation | Game Thread | 본 트랜스폼 최종 적용, 물리 바디 동기화 |
| GPU Skinning | Render Thread | 본 행렬 업로드, 버텍스 셰이더에서 스키닝 수행 |
UE5에서는 기본적으로 GPU Skinning을 사용합니다. 본 행렬을 Structured Buffer로 GPU에 전송하고, 버텍스 셰이더에서 스키닝을 수행합니다. 레이트레이싱이 활성화된 경우, BVH 업데이트를 위해 Recompute Skinning 옵션으로 레이트레이싱 전용 스키닝을 별도로 계산할 수 있습니다.
// 런타임에 특정 본의 월드 트랜스폼 가져오기
FTransform BoneWorldTransform = SkelMeshComp->GetBoneTransform(
SkelMeshComp->GetBoneIndex(FName("hand_r")));
// 컴포넌트 스페이스 본 트랜스폼 배열 접근
const TArray<FTransform>& ComponentSpaceTransforms =
SkelMeshComp->GetComponentSpaceTransforms();
// 강제 LOD 설정
SkelMeshComp->SetForcedLOD(1); // LOD 1 강제 사용
// MinLOD 설정 (이 LOD 이하로는 내려가지 않음)
SkelMeshComp->SetMinLOD(0);
핵심 요약
- USkeleton은 본 계층과 애니메이션 메타데이터를 관리하는 공유 애셋이며, 여러 USkeletalMesh가 하나의 USkeleton을 참조할 수 있다.
- FReferenceSkeleton은 본 이름, 부모 인덱스, 레퍼런스 포즈를 저장하는 핵심 데이터 구조체이다.
- USkeletalMesh의 렌더 데이터(FSkeletalMeshRenderData)는 LOD별로 분리되어 버텍스, 인덱스, 스킨 웨이트를 관리한다.
- LOD 시스템은 본 제거, 버텍스 축소, 스트리밍을 통해 다단계 최적화를 지원한다.
- USkeletalMeshComponent는 매 프레임 AnimInstance 업데이트 → AnimGraph 평가 → 본 트랜스폼 → GPU 스키닝 파이프라인을 실행한다.
도전 과제
배운 내용을 직접 실습해보세요
UE5에서 SK_Mannequin을 열고, Skeleton Tree에서 전체 본 계층을 확인하세요. LOD Settings에서 LOD 0/1/2의 버텍스 수와 Active Bone 수를 비교하고, LOD Auto Compute 설정을 실험하세요.
동일한 USkeleton을 참조하는 두 개의 USkeletalMesh(예: 바디 메시와 헤어 메시)를 준비하고, 하나의 애니메이션이 양쪽에 모두 적용되는지 확인하세요. Compatible Skeletons 설정도 시도해보세요.
C++에서 USkeletalMesh의 FSkeletalMeshRenderData에 접근하여 LOD별 버텍스 수, 삼각형 수, 본 수를 로그로 출력하는 디버그 함수를 작성하세요. 커스텀 LOD Screen Size 설정값에 따라 LOD 전환이 달라지는 것을 확인하세요.