← All Posts

UE5 Interchange 프레임워크 커스텀 트랜슬레이터 개발기: PMX 포맷 임포트

Study Notes
UE5 Interchange 프레임워크 커스텀 트랜슬레이터 개발기: PMX 포맷 임포트

들어가며

이전 프로젝트에서 Interchange를 커스텀하여 사내 에셋 파이프라인을 구축한 경험이 있지만, 보안 상 해당 작업을 공개하기는 어렵다. 그래서 공개 가능한 주제로 PMX(MikuMikuDance 모델) 포맷의 Interchange Translator를 처음부터 구현하고, 그 과정을 정리하여 공유하고자 한다.

다루는 내용:

  • Interchange 프레임워크의 아키텍처와 임포트 파이프라인
  • 커스텀 트랜슬레이터를 설계하고 구현하는 과정
  • PMX라는 바이너리 포맷을 SkeletalMesh로 변환하면서 마주친 기술적 판단들
  • 빌드 에러, 런타임 크래시 등 실제 삽질과 해결

1. Interchange 프레임워크란

Interchange Unreal Engine 5의 외부 에셋 임포트를 위한 범용 프레임워크이다.
3D 모델(FBX, glTF, OBJ, USD)뿐 아니라 텍스처(PNG, JPG, DDS, PSD 등), 오디오(WAV, MP3, FLAC 등),
머티리얼(MaterialX)까지 — 언리얼에서 파일을 임포트할 때 거치는 거의 모든 경로가 Interchange를 통한다.
Blueprint 혹은 Unreal C++를 사용하여 프로젝트에서 원하는대로 임포트 파이프라인을 구축할 수 있다.

1.1 왜 Interchange인가

UE5 이전에는 FBX 임포트가 UFbxFactory 하나에 집중되어 있었다.
파싱은 FFbxImporter가 담당했지만, 타입 감지부터 에셋 생성까지의 전체 흐름이 FactoryCreateFile() 하나(약 1,800줄)에 몰려 있어서 새 포맷을 추가하거나 임포트 동작을 커스텀하기가 어려웠다.

Interchange는 이 문제를 역할별로 분리한 프레임워크다:

flowchart LR
    A[소스 파일] --> B[Translator]
    B --> C[노드 그래프]
    C --> D[Pipeline]
    D --> C
    C --> E[Factory]
    E --> F[UE 에셋]

    subgraph C[노드 그래프]
        C1[Translated Node<br>메시·본·머티리얼·텍스처]
        C2[Factory Node<br>어떤 에셋을 만들지 지시]
    end

그리고 각 단계는 독립적으로 움직인다:

  • Translator — 소스 파일을 읽어 "이 파일에 무엇이 들어있는지"를 노드 그래프로 표현한다.
    새 포맷을 지원하려면 이것만 만들면 된다.
  • Pipeline — 노드 그래프를 가공하고, 어떤 UE 에셋을 만들지 결정한다.
    임포트 동작을 바꾸고 싶으면 이 단계를 교체하거나 확장한다. 예를 들어 특정 머티리얼을
    커스텀 셰이더로 대체하거나, 특정 본을 제외하는 등의 처리가 여기서 이루어진다.
  • Factory — Pipeline의 지시에 따라 실제 UE 에셋(SkeletalMesh, Material, Texture 등)을 생성한다.
    엔진에 이미 구현되어 있어서 커스텀 트랜슬레이터 개발자가 직접 만들 일은 거의 없다.

특히 새 포맷을 지원할 때 이점이 크다. Translator 하나만 구현하고 지원 확장자를 등록하면,
에디터의 드래그 앤 드롭·임포트 다이얼로그·에셋 브라우저가 해당 포맷을 자동으로 인식한다.
과거 UFactory 방식처럼 에디터 코드를 직접 수정할 필요가 없다.

1.2 노드 그래프 — Translator와 Factory의 중간 언어

Interchange의 핵심 아이디어는 노드 그래프다. UInterchangeBaseNodeContainer라는 플랫 맵(TMap<FString, Node*>)에 모든 노드가 들어간다.

노드는 두 종류로 나뉜다:

  • Translated Node — Translator가 생성. "이 파일에 메시가 하나 있고, 본이 100개 있고,
    머티리얼이 5개 있다"는 정보를 선언적으로 표현한다.
    • 예: UInterchangeSceneNode, UInterchangeMeshNode, UInterchangeShaderGraphNode, UInterchangeTexture2DNode
  • Factory Node — Pipeline이 생성. "이 Translated Node들을 조합해서 SkeletalMesh 에셋을 하나
    만들어라"는 지시.
    • 예: UInterchangeSkeletalMeshFactoryNode, UInterchangeMaterialFactoryNode

Translator 입장에서 중요한 것: 노드 그래프만 올바르게 구성하면 나머지는 엔진이 알아서 처리한다. Pipeline은 GenericAssetsPipeline이 기본으로 돌고, Factory는 SkeletalMesh/Material/Texture용이
이미 있다. 커스텀 트랜슬레이터 개발자가 Factory를 직접 만들 일은 거의 없다.

1.3 페이로드 — 지연 로딩 패턴

Translator는 Translate()에서 노드 그래프만 만들고, 실제 메시 데이터(MeshDescription)나 텍스처 픽셀은 아직 넘기지 않는다. 이 데이터는 나중에 Factory가 필요할 때 Translator의 페이로드 인터페이스를 호출해서 가져간다.

sequenceDiagram
    participant T as Translator
    participant N as 노드 그래프
    participant F as Factory

    T->>N: Translate() — 메시 노드 등록<br>PayloadKey = 'MyMesh' (선언만)
    F->>T: GetMeshPayloadData('MyMesh')<br>실제 MeshDescription 요청
    T-->>F: MeshDescription 반환

이 지연 로딩 덕분에 임포트 다이얼로그에서 사용자가 선택하지 않은 에셋은 실제로 로드하지 않아 메모리와 시간을 절약할 수 있다.


2. PMX 포맷 개요

PMX(Polygon Model eXtended)
:
MikuMikuDance(MMD)에서 사용하는 3D 모델 포맷이다. 바이너리 포맷이고, 버전 2.0/2.1이 주로 쓰인다.

2.1 데이터 구성

요소설명
버텍스Position, Normal, UV + 본 웨이트(BDEF1/2/4, SDEF, QDEF)
인덱스삼각형 인덱스 (1/2/4바이트 가변 크기)
텍스처 테이블상대 경로 문자열 배열 (실제 이미지 파일은 외부)
머티리얼Diffuse, Specular, Ambient, Toon 텍스처, SphereMap, 엣지
IK, 부여(Grant/Inherit) 본, 물리 후 변형 등 고유 계층
모프버텍스/UV/본/머티리얼/그룹 모프
리지드바디 & 조인트Bullet Physics 기반 물리 설정

3. 설계

3.1 참고 구현 — OBJ 트랜슬레이터

엔진에 포함된 OBJ 트랜슬레이터(UInterchangeOBJTranslator)가 가장 간결한 참고 구현이다. UInterchangeTranslatorBase 상속, Pimpl 패턴으로 내부 데이터 은닉, Translate()에서 노드 생성,
페이로드 인터페이스로 지연 로딩 — 이 뼈대를 기반으로 SkeletalMesh 특화 로직을 추가했다.

3.2 플러그인 구조

프로젝트 플러그인으로 개발했다. 엔진 코드를 수정하지 않고, 프로젝트의 Plugins/ 폴더에 독립 모듈로
존재한다.

plain text
InterchangePMX/
├── InterchangePMX.uplugin
├── Source/
│   ├── InterchangePMX.Build.cs
│   ├── Public/
│   │   ├── InterchangePMXTranslator.h    // 트랜슬레이터 클래스 선언
│   │   └── PMXParser.h                   // 바이너리 파서 + 데이터 구조
│   └── Private/
│       ├── InterchangePMXModule.cpp       // 모듈 등록
│       ├── InterchangePMXTranslator.cpp   // Translate() + 페이로드
│       └── PMXParser.cpp                  // 바이너리 파싱 구현

3.3 클래스 설계

c++
UCLASS(MinimalAPI, BlueprintType)
class UInterchangePMXTranslator : public UInterchangeTranslatorBase
    , public IInterchangeMeshPayloadInterface
    , public IInterchangeTexturePayloadInterface
{
    GENERATED_BODY()

public:
    virtual TArray<FString> GetSupportedFormats() const override;
    virtual EInterchangeTranslatorAssetType GetSupportedAssetTypes() const override;
    virtual EInterchangeTranslatorType GetTranslatorType() const override;
    virtual bool Translate(UInterchangeBaseNodeContainer& BaseNodeContainer) const override;

    // 메시 페이로드 (지연 로딩)
    virtual TOptional<UE::Interchange::FMeshPayloadData> GetMeshPayloadData(...) const override;

    // 텍스처 페이로드 (지연 로딩)
    virtual TOptional<UE::Interchange::FImportImage> GetTexturePayloadData(...) const override;

private:
    TPimplPtr<FPMXData> PMXDataPtr;  // 파싱 결과를 보관
};

IInterchangeMeshPayloadInterfaceIInterchangeTexturePayloadInterface를 동시에 구현해서,
하나의 트랜슬레이터가 메시와 텍스처 페이로드를 모두 제공한다.

3.4 핵심 설계 판단

결정선택이유
메시 타입SkeletalMeshPMX는 본/스키닝이 핵심. StaticMesh로는 의미가 없다
파서 분리FPMXData 별도 클래스바이너리 파싱과 노드 그래프 생성을 분리.
테스트와 유지보수가 용이
SupportedAssetTypesMeshes \| MaterialsTextures 플래그를 포함하면 BaseEngine.ini의 기본 설정에 의해 임포트 다이얼로그가 비활성화된다.
텍스처는 머티리얼 종속성으로 자동 임포트되므로 플래그 없이도 동작한다. FBX/glTF/OBJ 전부 동일한 패턴
SDEF 스키닝BDEF2로 폴백UE에 SDEF 네이티브 지원이 없다.
Deformer Graph로 확장 가능하지만 우선순위 밖
VMD 애니메이션별도 트랜슬레이터PMX=모델, VMD=애니메이션.
포맷 자체가 분리되어 있으므로 트랜슬레이터도 분리
물리(RigidBody/Joint)보류PhysicsAsset 매핑은 커스텀 파이프라인이 필요.
애니메이션 시스템 선행 필요

GetSupportedFormats()도 주의가 필요하다. 반환값은 "확장자;설명" 형식의 문자열 배열인데, 이 포맷이 틀리거나 누락되면 에디터의 파일 브라우저(Import 버튼으로 여는 창)에서 크래시가 발생할 수 있다.
드래그 앤 드롭으로만 테스트하면 이 경로를 지나지 않아서 발견이 늦어진다.

c++
// "확장자;설명" 형식을 정확히 지켜야 한다
return { TEXT("pmx;PMX Model File") };

4. 구현 — Translate()

Translate()는 트랜슬레이터의 핵심이다. PMX 파일을 읽어 노드 그래프를 구성한다.

4.1 전체 흐름

flowchart TD
    A[① PMX 바이너리 파싱] --> B[② 씬 루트 노드 생성]
    B --> C[③ 텍스처 노드 생성<br>PMX 텍스처 테이블 → Texture2DNode]
    B --> D[④ 머티리얼 노드 생성<br>ShaderGraphNode + Diffuse 텍스처 연결]
    B --> E[⑤ 스켈레톤 노드 생성<br>가상 Root + 본 계층 → Joint SceneNode]
    B --> F[⑥ 메시 노드 생성<br>MeshNode, SKELETAL 페이로드]
    B --> G[⑦ 액터 노드 생성<br>메시 인스턴스 씬 노드]
    F --> H[⑧ 모프 타겟 등록]

4.2 스켈레톤 — 가상 Root와 본 계층

PMX의 본 구조를 Interchange 노드로 변환하는 부분이 가장 까다로웠다.

PMX는 여러 루트 본이 존재할 수 있다(ParentIndex가 -1인 본이 여럿).
하지만 UE의 SkeletalMesh는 단일 루트 본을 요구한다. 그래서 가상의 Root 본을 하나 만들고, PMX에서
부모가 없는 본들을 전부 이 Root 아래에 붙였다.

c++
// 가상 Root 본 생성
UInterchangeSceneNode* RootJointNode = ...;
RootJointNode->SetCustomIsJoint(true);
RootJointNode->SetCustomLocalTransform(&BaseNodeContainer, FTransform::Identity);
RootJointNode->SetCustomBindPoseLocalTransform(&BaseNodeContainer, FTransform::Identity);

// 각 본 노드 생성
for (int32 i = 0; i < PMXData.Bones.Num(); ++i)
{
    const FPMXBone& Bone = PMXData.Bones[i];
    FString ParentUid = (Bone.ParentIndex >= 0)
        ? TEXT("\\\\Joint\\\\") + FString::FromInt(Bone.ParentIndex)
        : RootJointUid;  // 부모 없으면 가상 Root에 연결

    // 로컬 트랜스폼 = 자신의 월드 위치 - 부모의 월드 위치
    FVector BoneWorldPos = ConvertPMXPosition(Bone.Position);
    FVector ParentWorldPos = (Bone.ParentIndex >= 0)
        ? ConvertPMXPosition(PMXData.Bones[Bone.ParentIndex].Position)
        : FVector::ZeroVector;
    FTransform LocalTransform(FQuat::Identity, BoneWorldPos - ParentWorldPos);

    BoneNode->SetCustomLocalTransform(&BaseNodeContainer, LocalTransform);
    BoneNode->SetCustomBindPoseLocalTransform(&BaseNodeContainer, LocalTransform);
}

처음에는 SetCustomLocalTransform만 설정하고 SetCustomBindPoseLocalTransform을 빠뜨렸는데, 임포트 도중 스켈레탈 메시 팩토리 내부에서 ensure 크래시가 발생했다.
팩토리가 GetCustomBindPoseGlobalTransform을 호출할 때 BindPoseLocalTransform → LocalTransform 순으로 폴백하는데, 특정 경로에서 둘 다 없으면 크래시가 난다.
모든 SceneNode(루트, 본, 메시 인스턴스)에 두 트랜스폼을 반드시 설정해야 한다.

4.3 텍스처 & 머티리얼 — 종속성 체인

PMX의 텍스처는 파일 내에 직접 포함되지 않고, 텍스처 경로 테이블(상대 경로 문자열 배열)로 관리된다.
실제 이미지 파일은 PMX 파일과 같은 폴더에 위치한다.

노드 그래프에서의 종속성:

flowchart TB
    A[UInterchangeTexture2DNode<br>텍스처] --> B[UInterchangeShaderGraphNode<br>머티리얼 — Diffuse 텍스처 연결]
    B --> C[UInterchangeMeshNode<br>메시 — 머티리얼 슬롯 연결]

이 종속성을 올바르게 연결하면, Pipeline과 Factory가 Texture → Material → Mesh 순서로 에셋을 생성한다.

4.4 모프 타겟 — PMX 페이셜을 UE에서 쓰기까지

PMX 모델에는 표정용 버텍스 모프가 포함되어 있다(눈 깜빡임, 입 모양 등). 이걸 UE의 MorphTarget으로 매핑해서 에디터에서 슬라이더로 조작할 수 있게 하는 것 또한 이번 R&D 목표 중 하나였다.

Interchange에서의 처리 흐름:

  • Translate() — 모프 이름만 MeshNode에 등록 (페이로드 지연 로딩)
  • GetMeshPayloadData() — MeshDescription에 모프별 버텍스 위치를 채워서 반환

처음에는 동작하지 않았다. PMX의 모프 데이터는 "영향 받는 버텍스의 오프셋"만 저장하고 있어서, 그대로 MeshDescription에 넣었더니 모프를 적용하면 모델 전체가 원점으로 수축하는 현상이 발생했다.

원인은 Interchange의 팩토리가 모프 위치를 절대 좌표로 해석하기 때문이다:

plain text
delta = morphTargetPos - baseMeshPos

영향 없는 버텍스를 ZeroVector로 넣으면 0 - basePos가 되어 모든 버텍스가 원점으로 끌려간다.
수정은 간단했다 — 모든 버텍스를 베이스 메시 위치로 초기화한 뒤, 영향 받는 버텍스에만 오프셋을 더한다.

c++
// 모든 버텍스를 베이스 위치로 초기화
for (int32 i = 0; i < VertexCount; ++i)
    MorphPositions[i] = ConvertPMXPosition(BaseVertices[i].Position);

// 영향 받는 버텍스에만 오프셋 추가
for (const auto& [VertexIndex, Offset] : Morph.VertexOffsets)
    MorphPositions[VertexIndex] += ConvertPMXPosition(Offset);
모프 타겟 테스트 A
모프 타겟 테스트 A
모프 타겟 테스트 B
모프 타겟 테스트 B

PMX에는 버텍스 모프 외에도 본 모프, 머티리얼 모프, 그룹 모프 등이 있지만, Interchange의 MorphTarget은 버텍스 오프셋만 지원한다. 나머지 모프 타입은 커스텀 파이프라인에서 별도 처리가 필요하다.


5. 구현 — 바이너리 파서

5.1 PMX 바이너리 구조

PMX는 헤더부터 각 섹션을 순서대로 읽어야 하는 바이너리 포맷이다. 특정 섹션으로 바로 건너뛸 수 없고,
앞 섹션의 크기를 계산해야 다음 위치를 알 수 있다.

flowchart LR
    A[헤더<br>매직넘버 · 버전 · 글로벌 설정] --> B[모델 정보<br>이름 · 코멘트]
    B --> C[지오메트리<br>버텍스 · 인덱스 · 텍스처]
    C --> D[머티리얼 · 본 · 모프]
    D --> E[물리<br>리지드바디 · 조인트]

헤더의 글로벌 설정에는 "본 인덱스를 몇 바이트로 읽을지", "텍스처 인덱스를 몇 바이트로 읽을지" 같은
인덱스 크기가 종류별로 정의되어 있다(1/2/4바이트).
예를 들어 본이 256개 이하인 모델은 본 인덱스를 1바이트로 저장하고, 그 이상이면 2바이트나 4바이트를
쓴다. 이 설정은 파일 전체에 걸쳐 적용되기 때문에, 헤더를 먼저 읽어 인덱스 크기를 파악해야 이후 섹션을
올바르게 파싱할 수 있다.

5.2 가변 크기 인덱스 읽기

5.1에서 설명한 것처럼, PMX의 인덱스는 모델에 따라 1/2/4바이트로 크기가 달라진다.
하나의 함수에서 크기별로 분기해서 읽어야 한다.

c++
int32 FPMXParser::ReadIndex(FMemoryReader& Ar, int32 IndexSize)
{
    switch (IndexSize)
    {
    case 1: { int8  V; Ar << V; return (V == -1) ? -1 : V; }
    case 2: { int16 V; Ar << V; return (V == -1) ? -1 : V; }
    case 4: { int32 V; Ar << V; return V; }
    }
    return -1;
}

여기서 주의할 점은 부호 있는 타입(int8, int16)으로 읽어야 한다는 것이다.
PMX에서 -1은 "참조 없음"을 의미하는데(예: 부모 본이 없는 루트 본), 1바이트에서는 0xFF, 2바이트에서는 0xFFFF가 -1에 해당한다. 부호 없는 타입으로 읽으면 이 값이 255나 65535로 해석되어 존재하지 않는 인덱스를 참조하게 된다.

5.3 스키닝 웨이트 — 다양한 디포메이션 타입

스키닝이란 메시의 각 버텍스가 어떤 본에 얼마나 영향받는지를 정의하는 것이다. 예를 들어 팔꿈치 근처 버텍스는 상완 본과 하완 본에 50%씩 영향받아서, 팔을 구부리면 자연스럽게 따라 변형된다.

PMX는 버텍스마다 스키닝 방식이 다를 수 있다:

타입본 수설명
BDEF11하나의 본에 100% 종속 (손가락 끝 등 단순한 부위)
BDEF22두 본 사이를 웨이트로 블렌딩 (관절 부위)
BDEF44최대 4개 본의 영향을 받음 (어깨 등 복잡한 부위)
SDEF2BDEF2와 같지만, 관절 꺾임이 자연스러운 구면 보간 적용
QDEF4BDEF4와 같지만, 쿼터니언 기반 보간 적용 (PMX 2.1)

BDEF 계열은 UE의 스키닝과 동일한 방식이라 그대로 매핑하면 된다. 문제는 SDEF와 QDEF인데, 이 두 방식은 UE가 네이티브로 지원하지 않는 보간 알고리즘을 사용한다. SDEF는 본 웨이트 자체는 BDEF2와 동일하므로 추가 보간 파라미터(C, R0, R1)를 무시하고 BDEF2로 폴백했다. 관절부 변형이 약간 딱딱해지지만, 기본적인 스키닝은 정상 동작한다.


6. 좌표계 변환

임포트 파이프라인에서 가장 실수가 잦고 디버깅이 까다로운 부분이다.

6.1 PMX vs UE 좌표계

PMXUE
전방-Z+X
우측+X+Y
상단+Y+Z
핸드니스좌손좌손

둘 다 좌손 좌표계지만 축 배치가 다르다. 변환 함수:

c++
// PMX(X, Y, Z) → UE(X, -Z, Y)
FVector ConvertPMXPosition(const FVector3f& V)
{
    return FVector(V.X, -V.Z, V.Y) * PMXToUEScale;
}

// 노멀은 스케일 미적용
FVector ConvertPMXNormal(const FVector3f& V)
{
    return FVector(V.X, -V.Z, V.Y);
}

6.2 스케일

PMX의 1단위는 약 8cm에 해당한다 (일반적인 MMD 캐릭터 키 ~20단위 = 160cm).
UE는 1단위 = 1cm이므로 8배 스케일을 적용했다.

c++
constexpr float PMXToUEScale = 8.0f;
좌: 단위 인지 전 애셋 임포트, 우: 수정 후 애셋 임포트
좌: 단위 인지 전 애셋 임포트, 우: 수정 후 애셋 임포트

6.3 UV 좌표

PMX와 UE 모두 좌상단 원점의 UV 좌표를 사용하므로 변환이 필요 없다. glTF처럼 V축 반전이 필요한 포맷도 있는데, PMX는 그냥 그대로 쓰면 된다.

6.4 와인딩 순서

PMX는 DirectX 기반이라 시계방향(CW) 와인딩을 사용하고, UE는 반시계방향(CCW) 와인딩을 사용한다.
그대로 넣으면 일부 면의 노멀이 뒤집혀 보인다. 삼각형 인덱스 순서를 0,1,20,2,1로 반전해야 한다.

c++
// PMX CW → UE CCW
for (int32 i = 0; i < Indices.Num(); i += 3)
    Swap(Indices[i + 1], Indices[i + 2]);

Blender의 mmd_tools 플러그인도 임포트 시 동일하게 와인딩을 뒤집는다.

블렌더에서 임포트한 pmx 파일, 붉은색으로 보이는 건 면이 뒤집힌 상태를 의미함
블렌더에서 임포트한 pmx 파일, 붉은색으로 보이는 건 면이 뒤집힌 상태를 의미함
와인딩 수정 전
와인딩 수정 전
와인딩 수정 후
와인딩 수정 후

6.5 본 트랜스폼에서의 좌표 변환

위치뿐 아니라 본의 로컬 트랜스폼도 변환해야 한다. PMX 본은 월드 좌표(Position)로 저장되어 있어서,
로컬 트랜스폼은 자신의 월드 위치 - 부모의 월드 위치로 계산한다. 이 계산을 좌표 변환 후에 해야 축이 올바르게 적용된다.


7. 결과

7.1 구현된 기능

기능상태
PMX 바이너리 파싱 (헤더, 버텍스, 인덱스, 텍스처, 머티리얼, 본, 모프)완료
SkeletalMesh 임포트 (메시 + 스켈레톤 + 스키닝)완료
머티리얼 & 텍스처 임포트완료
모프 타겟 (표정 블렌드셰이프)완료
좌표계 변환 & 스케일 보정완료

7.2 향후 확장 가능 영역

영역접근 방법
VMD 애니메이션별도 Interchange 트랜슬레이터로 구현
물리 (RigidBody/Joint)커스텀 파이프라인에서 PhysicsAsset 매핑
Toon/SphereMap 머티리얼커스텀 파이프라인에서 전용 머티리얼 인스턴스 생성
SDEF 커스텀 스키닝Deformer Graph 확장
IK/부여 본Control Rig 매핑

8. 정리

Interchange 프레임워크로 커스텀 트랜슬레이터를 만드는 것은 노드 그래프의 구조를 이해하는 것이 핵심
이다. 어떤 노드를 만들어야 하고, 노드 간 관계(부모-자식, 종속성)를 어떻게 연결해야 하는지를 파악하면,
나머지는 엔진의 Pipeline과 Factory가 처리해준다.

PMX라는 하나의 포맷을 구현하는 과정에서 다음 영역들을 다뤘다:

  • 바이너리 파싱 — 바이너리 스트림을 직접 읽는 파서를 별도 클래스로 분리하는 설계
  • SkeletalMesh 파이프라인 — 본 계층, 스키닝 웨이트, 모프 타겟까지 Interchange의 스켈레탈 파이프라인을 전부 활용
  • 좌표계 변환 — 위치, 노멀, 본 트랜스폼, 와인딩 순서 각각에 변환 로직이 필요.
    하나라도 틀리면 메시가 뒤틀린다
  • 에셋 종속성 체인 — 텍스처 → 머티리얼 → 메시의 종속성을 노드 그래프로 표현하고,
    Factory가 올바른 순서로 에셋을 생성하도록 구성
  • Interchange API 탐색 — 문서가 부족해서 엔진 소스(OBJ/FBX/glTF 트랜슬레이터)를 직접 읽어야
    했다. 클래스 소속이나 네임스페이스가 직관적이지 않아서
    (ConnectDefaultOutputToInputShaderGraphNode이 아닌
    UInterchangeShaderPortsAPI에 있는 식) 새 API를 쓸 때마다 실제 선언을
    검색해야 했다

Interchange의 강점은 확장성이다.
트랜슬레이터 하나만 등록하면 에디터 통합(드래그 앤 드롭, 임포트 다이얼로그, 에셋 브라우저)이 자동으로 따라온다. 과거의 UFbxFactory 방식처럼 에디터 코드를 일일이 수정할 필요가 없다.
새로운 포맷의 에셋 파이프라인을 구축해야 한다면, Interchange는 충분히 검토할 가치가 있다.