역자의 말:
저자 YOung 씨가 AI 와 함께 작성 한 글이라 직접 알아볼까 하다가 이게 AI 와 함께 문서를 작성하더라도 저자의 질의 관점에 따라서 내용이 달라지는 관계로 그대로 번역글로 옮겼습니다.
또한 자칫 혼동 할 수 있기 때문에 글 내용이 다루는 AnimBank는 이름만 보면 언리얼의 Anim Budget과 혼동하기 쉽습니다. 둘 다 많은 캐릭터가 등장하는 장면에서 애니메이션 비용을 줄이려는 문제의식에서 출발한다는 점은 비슷하지만, 실제로 맡고 있는 역할과 접근 방식은 상당히 다릅니다. Anim Budget은 기존 SkeletalMeshComponent와 AnimBP 구조를 유지한 상태에서, 캐릭터별 애니메이션 업데이트 빈도와 평가 비용을 조절하는 최적화 장치에 가깝습니다. 반면 이 글에서 말하는 AnimBank는 애니메이션 예산을 관리하는 시스템이라기보다, UAnimSequence를 GPU에서 사용하기 좋은 형태로 미리 변환해 두는 데이터 뱅크에 가깝습니다. 글에서도 AnimBank가 애니메이션 시퀀스를 프레임별 위치·회전 키와 바운딩 박스로 컴파일하고, 각 인스턴스가 AnimationIndex를 통해 특정 시퀀스를 참조한다고 설명하고 있습니다. 따라서 이 글의 핵심은 “캐릭터마다 AnimBP 평가 비용을 어떻게 줄일 것인가”라기보다는, 단순한 군중 캐릭터라면 애초에 개별 SkeletalMeshComponent와 AnimBP 구조를 사용하지 않고, 인스턴스화된 스키닝 메시와 사전 베이킹된 애니메이션 데이터, 그리고 GPU Compute 기반 스키닝 구조로 처리하자는 데 있습니다. 정리하면, Anim Budget과 ISKM/AnimBank는 모두 다수 캐릭터의 본 애니메이션 업데이트 비용을 줄이려는 맥락에 있지만, 해결 방식은 완전히 다릅니다. Anim Budget은 기존 애니메이션 시스템 위에서 비용을 조절하는 장치이고, 이 글의 SKM/AnimBank는 다수의 단순 캐릭터를 위해 별도로 설계된 GPU 기반 인스턴싱·스키닝 구조라고 이해하는 편이 정확합니다.
저자: YOung
읽기 전 안내: 이 글의 대부분 내용은 Claude Code + DeepSeek V4 Pro로 생성되었습니다.
들어가며
UE 5.6은 UE_EXPERIMENTAL로 표시된 새로운 컴포넌트인 UInstancedSkinnedMeshComponent(ISKM)를 도입했습니다. 이 컴포넌트가 해결하려는 핵심 문제는 명확합니다. 화면 안에 수백 명의 캐릭터가 있을 때 병목은 렌더링이 아니라, CPU 쪽에서 Actor마다 수행되는 골격 애니메이션 평가라는 점입니다.
이 글은 UE 5.7.4 소스 코드 분석을 바탕으로, MassCrowd 플러그인 통합 실전 경험을 더해 ISKM의 아키텍처 설계, 렌더링 파이프라인, 애니메이션 시스템, 그리고 현재의 제약을 깊게 살펴봅니다.
1. 핵심 아키텍처
1.1 CPU 애니메이션 비용 0
ISKM과 전통적인 SkeletalMeshComponent의 근본적인 차이는 다음과 같습니다. CPU 쪽에서 골격 계산을 전혀 하지 않습니다.
SkeletalMeshComponent:
TickComponent → TickPose → RefreshBoneTransforms → ComponentSpaceTransforms 계산
→ SendRenderDynamicData → 골격 행렬을 GPU로 업로드
InstancedSkinnedMeshComponent:
TickComponent → Super만 호출
RefreshBoneTransforms → 빈 함수, 바운드만 Dirty로 표시
SendRenderInstanceData → 인스턴스 Transform + AnimationIndex만 동기화
각 인스턴스는 두 가지 데이터만 저장합니다(InstancedSkinnedMeshComponent.h:22-51).
struct FSkinnedMeshInstanceData
{
FTransform3f Transform; // 3×4 행렬. 구버전 FMatrix보다 절반 정도 절약
uint32 AnimationIndex; // AnimBank 애니메이션 시퀀스를 가리키는 정수 인덱스
};
인스턴스가 100개라면 매 프레임 동기화해야 하는 것은 100개의 FTransform3f와 100개의 uint32뿐입니다. 골격 계층 순회, ABP 상태 머신, 골격 블렌딩, IK는 없습니다.
1.2 AnimBank: 사전 베이킹 애니메이션 시스템
UAnimBank(AnimBank.h:177-282)는 전통적인 UAnimSequence를 GPU 친화적인 프레임별 골격 데이터로 컴파일합니다.
UAnimSequence → FAnimBankBuildAsyncCacheTask → FAnimBankData
├── FSkinnedAssetMapping (골격 리타깃 테이블)
└── FAnimBankEntry[] (사전 베이킹)
├── PositionKeys[] (float3/프레임)
├── RotationKeys[] (quat4/프레임)
└── SampledBounds (프레임별 바운딩 박스)
UAnimBankData(AnimBank.h:613-638)는 UTransformProviderData의 하위 클래스로, AnimationIndex를 구체적인 UAnimBank 애셋과 시퀀스 번호에 매핑합니다.
1.3 GPU 측 스키닝 파이프라인
게임 스레드:
SendRenderInstanceData_Concurrent()
→ InstanceDataManager.FlushChanges()
→ Transform + AnimationIndex를 GPU Scene에 업로드
렌더 스레드:
FSkinningSceneExtension::PerformSkinning():
├── FAnimBankTransformProvider::ProvideGPUBankTransforms()
│ ├── AdvanceAnimation() ← 재생 시간 진행
│ ├── FAnimBankEvaluateCS ← 키프레임 샘플링, 인스턴스별 골격 출력
│ └── FAnimBankScatterCS ← 오프셋에 따라 전역 TransformBuffer에 Scatter
└── Skinning Compute Shader ← TransformBuffer를 소비해 모든 인스턴스 스키닝
핵심 설계는 다음과 같습니다. AnimBank 안의 20개 인스턴스는 Evaluate 1회 + Scatter 1회 + Skinning 1회를 공유합니다. 20번 독립 호출하는 구조가 아닙니다.
2. 왜 Nanite를 먼저 지원하는가
Non-Nanite 경로의 코드는 이미 존재하지만, 하나의 매크로로 통째로 꺼져 있습니다(SkinningDefinitions.h:65).
#define USE_SKINNING_SCENE_EXTENSION_FOR_NON_NANITE 0
이 매크로는 약 10개 파일에 걸친 핵심 코드 경로를 비활성화합니다. 여기에는 인스턴스 스키닝 셰이더 컴파일, GPU 스키닝 데이터 파이프라인, Non-Nanite 레이 트레이싱 지원이 포함됩니다.
우선순위는 결국 필요한 공수로 결정됩니다.
Nanite 경로 Non-Nanite 경로
| 스키닝 Compute | 신규 추가(인터페이스 1개) | 신규 추가 |
| GPU Culling | ✅ Cluster 계층 재사용 | ❌ 새로 만들어야 함 |
| LOD 선택 | ✅ Cluster 계층 재사용 | ❌ 인스턴스별 LOD를 새로 만들어야 함 |
| 래스터라이즈 | ✅ VisBuffer 재사용 | ❌ 간접 드로우를 새로 만들어야 함 |
| 레이 트레이싱 | ✅ 정적 Geometry 재사용 | ❌ checkNoEntry() |
Nanite가 이미 갖고 있는 GPU-driven 파이프라인(Cluster Culling, VisBuffer 래스터라이즈, ExecuteIndirect)은 ISKM에 거의 투명하게 동작합니다. 렌더링 전에 스키닝 Compute Pass 하나만 추가하면 됩니다. 반면 Non-Nanite는 사실상 렌더링 파이프라인의 절반을 다시 작성해야 합니다.
Non-Nanite 골격 모델은 ISKM의 GPU 스키닝을 사용하더라도 재질 Section별 DrawCall 병목을 피할 수 없습니다. 래스터라이즈 파이프라인에서는 CPU가 매번 그리기 전에 Shader, 재질, 버텍스 버퍼를 바인딩해야 하기 때문입니다. 100명이면 최소 100번의 DC가 필요합니다. 반면 Nanite의 재질 전환은 픽셀 단위에서 일어납니다. VisBuffer가 각 픽셀의 TriangleID를 기록하고, 테이블 조회를 통해 해당 삼각형의 재질을 얻은 뒤, BasePass가 스크린 공간에서 픽셀 단위로 셰이딩합니다. 100명도 1회의 ExecuteIndirect로 처리할 수 있습니다. 그래서 ISKM이 Nanite를 먼저 지원하는 것은 선호의 문제가 아니라 아키텍처의 문제입니다.
3. 렌더링 배치 분석
3.1 스키닝 단계(독립)
각 Nanite Primitive는 독립적으로 스키닝됩니다. 여기에서 SKM과 ISKM은 갈라집니다.
SKM Actor (10개):
CPU ABP 평가 → UploadBuffer(Bone) → Skinning CS → ObjectSpaceBuffer
ISKM Component (컴포넌트 1개, 인스턴스 20개):
AnimBankEvaluate → AnimBankScatter → Skinning CS → ObjectSpaceBuffer
RenderDoc에서는 SKM의 경우 UpdateBuffer(골격 행렬 업로드)를 볼 수 있고, ISKM의 경우 AnimBankEvaluate와 AnimBankScatter라는 전용 Compute Pass 두 개를 볼 수 있습니다.
3.2 래스터라이즈 단계(배치 처리)
스키닝이 끝나면 모든 Primitive의 스키닝된 버텍스 데이터는 통합되어 Nanite 파이프라인으로 들어갑니다.
Compute Shader: BuildCullingArgs
→ 모든 Primitive의 모든 Cluster가 함께 Culling에 참여
→ 살아남은 Cluster 목록을 IndirectArgs Buffer에 기록
ExecuteIndirect: RasterizeSurvivingClusters
→ 하나의 Dispatch가 모든 가시 삼각형을 그림
→ VisBuffer {PrimitiveID, TriangleID, Depth}에 기록
BasePass (IndirectDispatch):
→ 스크린 공간 순회, 픽셀별로 VisBuffer 읽기 → 재질 해석
RenderDoc에서는 하나의 IndirectDispatch만 보입니다. 여기에는 장면 안의 모든 Nanite 콘텐츠(SKM + ISKM + 정적 Nanite 메시)의 래스터라이즈가 포함됩니다.
4. 현재 제약
4.1 LOD0 Only
두 렌더링 경로의 GetLOD()가 모두 0으로 하드코딩되어 있습니다.
FInstancedSkeletalMeshObjectNanite::GetLOD() → return 0;
FInstancedSkeletalMeshObjectGPUSkin::GetLOD() → return 0;
소스 코드 주석에서도 이를 확인할 수 있습니다.
// TODO: Support LOD switching - needs dynamic data updates.
(SkinningSceneExtensionProxy.cpp:22)
4.2 AnimBank는 애니메이션 블렌딩을 지원하지 않음
FSkinnedMeshInstanceData에는 AnimationIndex 하나만 있습니다. 즉, 여러 애니메이션을 동시에 재생하고 블렌딩하는 기능은 지원하지 않습니다. ABP 상태 머신, Blend Space, IK, AnimNotify도 없습니다.
4.3 Non-Nanite 경로는 사용할 수 없음
매크로로 비활성화되어 있을 뿐 아니라, Non-Nanite 경로의 FInstancedSkeletalMeshObjectGPUSkin::Update()는 빈 함수이고, FInstancedSkinnedMeshSceneProxy::GetDynamicRayTracingInstances()는 곧바로 checkNoEntry()를 호출합니다.
4.4 공개된 SetInstanceTransform API가 없음
위치 업데이트는 RemoveInstance() + AddInstance() 조합으로만 구현할 수 있습니다. 그때마다 FPrimitiveInstanceId 매핑을 다시 만들어야 합니다.
5. MassCrowd 통합 실전
우리는 ISKM을 MassCrowd 플러그인에 통합해, LOD 기반의 3단계 렌더링 전략을 구현했습니다.
5.1 LOD 전략
High LOD (0~500cm): SpawnedActor + SkeletalMeshComponent
→ Actor 10개, 완전한 ABP, 상태 머신/Blend/IK 모두 지원
Medium LOD (500~2000cm): ISKM + AnimBank
→ 인스턴스 20개, GPU 스키닝, CPU 애니메이션 비용 0
Low LOD (2000~5000cm): ISM (StaticMeshInstance)
→ 정적 메시 인스턴싱, 애니메이션 없음
5.2 신규 컴포넌트
MassCrowd 플러그인 내부에 다음 요소들을 추가했습니다. MassRepresentation/MassGameplay 모듈은 수정하지 않습니다.
컴포넌트 기능
| UMassCrowdISKMSubsystem | WorldSubsystem. ISKM 컴포넌트 생성과 Entity↔InstanceId 매핑 관리 |
| FMassCrowdISKMFragment | Entity별 Mass Fragment. ISKM 인스턴스 상태 추적 |
| UMassCrowdISKMUpdateProcessor | Mass Processor. 매 프레임 Entity Transform을 ISKM 인스턴스에 동기화 |
| UMassCrowdISKMFragmentDestructor | Mass Observer. Entity가 파괴될 때 대응되는 ISKM 인스턴스 정리 |
6. 정리
ISKM의 핵심 설계 철학은 골격 애니메이션을 CPU에서 GPU로 옮기는 것입니다. 이를 위해 AnimBank 사전 베이킹과 Nanite GPU-driven 렌더링 파이프라인을 활용합니다. ISKM은 SkeletalMeshComponent의 대체품이 아니라, Crowd나 Mass Entity처럼 “단순한 애니메이션을 가진 다수의 캐릭터” 장면을 위해 설계된 전용 도구입니다.
병목이 GPU 래스터라이즈가 아니라 CPU 측 N회 ABP 평가라면, ISKM의 가치는 분명해집니다. N개 인스턴스의 애니메이션 비용이 N회의 CPU 계산에서 1회의 GPU Compute Shader 디스패치로 줄어들기 때문입니다.
현재 버전(5.7.4)은 여전히 Experimental로 표시되어 있으며, Non-Nanite 경로, LOD 전환, 애니메이션 블렌딩, 레이 트레이싱은 아직 완성되지 않았습니다. 하지만 Nanite 골격 메시를 사용하는 단순 애니메이션 군중 장면에서는 이미 기능적으로 사용할 수 있는 수준입니다.
이 글은 UE 5.7.4 소스 코드 분석을 기반으로 합니다. 모든 파일 경로와 줄 번호는 이 글을 작성하던 시점의 엔진 버전에 대응합니다.
원문
(72 封私信 / 30 条消息) UE5 InstancedSkinnedMeshComponent 技术深度解析 - 知乎
'TECH.ART.FLOW.IO' 카테고리의 다른 글
| [번역] 넷이즈 레이훠(LGDC) 시리즈|모바일 파이프라인 상수 소개 및 구현 공유 (0) | 2026.05.24 |
|---|---|
| [번역] UE5에서 성능 좋은 서브서피스 옥(翡翠) 표현하기 (0) | 2026.05.24 |
| [번역] 언리얼엔진5의 GameplayGraph (0) | 2026.05.19 |
| [번역] 넷이즈 레이훠 LGDC 시리즈 《영겁무간 모바일》RenderGraph 개조 공유 (2) | 2026.05.15 |
| [번역] 톤매핑만으로는 부족하다: 로컬 톤매핑 방안 정리 (0) | 2026.05.15 |