포스트

[TIL] 2026-06-24 — VibeUE(언리얼 MCP)로 펭귄 캐릭터 임포트 · 스케일·크래시·리타게팅 삽질기

[TIL] 2026-06-24 — VibeUE(언리얼 MCP)로 펭귄 캐릭터 임포트 · 스케일·크래시·리타게팅 삽질기

이날의 큰 줄기는 VibeUE(언리얼 MCP) 에이전트를 설치하고, 그걸로 펭귄 캐릭터를 임포트→셋업→애니메이션까지 붙이는 과정이었다. 자연어로 에디터를 조작하는 신세계였지만, 그만큼 파이썬 자동화의 함정(CDO 직접 수정 크래시, 레벨 빠른전환 크래시), FBX/GLB 임포트 스케일 미스매치, 8K 텍스처로 인한 VRAM 고갈, 그리고 cross-rig 리타게팅의 한계까지 — 함정을 하나씩 밟으며 원인과 해법을 정리했다. 곁들여 협동 운반 멀티플레이의 ATCCarriableFurniture C++ 베이스도 작성했다.

오늘 한 일 요약

  1. VibeUE(언리얼 MCP) 플러그인 설치 + 2단계 MCP 도구 구조 파악 + 팀에 안 새는 로컬 전용 git 설정
  2. 네트워크 C++ 토대 빌드 검증(리슨서버 PIE) + 협동 운반 베이스 ATCCarriableFurniture 작성
  3. Tripo GLB 6종 임포트 + 팀 컨벤션(도메인 폴더·접두사) 적용
  4. 새 레벨이 어두운 원인 = DirectionalLight pitch 부호 → Rotator 인자순서(roll,pitch,yaw) 함정 규명
  5. 펭귄 FBX(78본 CC 리그) 임포트 — 1cm 크기 함정 + 퐁 머티리얼 광택 보정
  6. 파이썬 자동화의 CDO 직접 수정 / 레벨 빠른전환 크래시 안전 패턴 정립
  7. BP_PenguinCharacter 제작 + PIE PC null 크래시 → DefaultPawnClass 스폰 경로로 해결
  8. 8K 텍스처 VRAM 고갈max_texture_size=1024
  9. 애니메이션 리타게팅 대장정 — cross-rig 붕괴 → 동일 스켈레톤 애님 임포트 + root 본 Skeleton 리타게팅으로 in-place 처리

1. VibeUE(언리얼 MCP) 에이전트 설치 + 로컬 전용 git 설정

VibeUE란

kevinpbuckley/VibeUE를 프로젝트 Plugins/에 git clone 했다. UE 5.8의 네이티브 MCP 서버를 확장하는 플러그인으로, 블루프린트·머티리얼·애니메이션·지형·위젯·성능 프로파일링 등 30+ 서비스를 제공한다. 핵심은 자연어로 시키면 Claude가 파이썬/툴 호출로 에디터를 조작한다는 것.

MCP 도구 구조가 2단계라는 점이 인상적이었다.

  • 메타도구 3개: list_toolsetsdescribe_toolsetcall_tool (필요한 툴셋을 탐색하고 그 안의 툴을 호출)
  • 또는 execute_python_code에디터 프로세스 안에서 직접 파이썬 실행

도구 수십 개를 LLM 컨텍스트에 다 올리지 않고, “툴셋 목록 → 선택 → 호출”로 점진 로드하는 설계라 토큰 효율이 좋다.

설치는 .uproject에 VibeUE 플러그인 추가 + 에디터에서 C++ 모듈 컴파일이 필요하다.

팀에 안 새는 로컬 전용 git 설정

VibeUE는 개인 도구라 팀 저장소에 커밋되면 안 된다. 추적 여부에 따라 두 방법을 나눠 썼다.

대상상태방법
TeamCarry.uproject (플러그인 추가됨)이미 추적 중git update-index --skip-worktree 로 내 수정만 커밋 제외
.mcp.json, Plugins/VibeUE/미추적.gitignore(공유됨) 대신 .git/info/exclude(로컬 전용)에 추가
1
2
3
4
5
# 추적 파일의 내 로컬 수정만 무시 (다른 사람 .uproject 변경은 정상 추적)
git update-index --skip-worktree TeamCarry.uproject

# 미추적 항목은 로컬 전용 exclude로 (팀에 공유되는 .gitignore 대신)
printf "/.mcp.json\n/Plugins/VibeUE/\n" >> .git/info/exclude

skip-worktree 주의점: 팀원이 같은 파일(.uproject)을 수정해서 pull 충돌이 나면, git update-index --no-skip-worktree로 잠깐 풀고 병합한 뒤 다시 --skip-worktree로 잠가야 한다. skip-worktree는 “내 로컬 변경을 git이 못 본 척”하는 거라, 원격 변경과 합쳐질 때 일시 해제가 필요하다.

2. 네트워크 토대 검증 + ATCCarriableFurniture C++

이전에 깔아둔 네트워크 C++ 토대(UTCNetStatics 권위/역할 헬퍼, UTCCarryStatics::CombineCarryVelocity 합산 물리, ITCGrabbable 인터페이스, UTCGameInstance의 HostListenServer/JoinByAddress)를 빌드 검증하고 리슨서버 PIE 세션 진입까지 확인했다.

이어서 운반 가구의 C++ 베이스 ATCCarriableFurniture를 작성했다.

  • ITCGrabbable(운반 계약)과 ITCInteractable(잡기 진입)을 동시 구현 — 역할 분리(상태/물리 vs 포커스/입력)는 이전 설계 그대로.
  • 복제 변수 3종:
    • bIsGrabbed (RepNotify — 잡힘 연출을 모든 클라 일관 처리)
    • GrabbingPlayers (현재 운반자 목록)
    • CombinedVelocity (합산된 이동 벡터)
  • 서버 권위 틱에서 운반자 입력을 합산해 이동시키고 결과를 복제. GrabComponent의 잡기 흐름과 연결된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 개념 골격 — 서버에서만 합산·이동, 클라는 OnRep으로 연출
void ATCCarriableFurniture::Tick(float Dt)
{
    Super::Tick(Dt);
    if (!HasAuthority() || !bIsGrabbed) return;

    TArray<FVector> Inputs;
    for (ATCPlayerCharacter* P : GrabbingPlayers)
        Inputs.Add(P->GetCarryMoveInput());

    CombinedVelocity = UTCCarryStatics::CombineCarryVelocity(
        Inputs, GetRequiredCarriers(), BaseCarrySpeed);   // 순수 함수 재사용
    AddActorWorldOffset(CombinedVelocity * Dt, true);     // 물리 대신 단순 이동(동기화 단순)
}

void ATCCarriableFurniture::OnRep_IsGrabbed()             // 클라 연출 일관
{
    /* 잡힘 하이라이트/사운드 */
}

3. GLB 가구 에셋 임포트 + 컨벤션

Tripo로 만든 GLB 6종(냉장고/소파/TV/의자/박스/펭귄)을 AssetImportTask로 자동 임포트한 뒤 팀 컨벤션(team-rules.md)을 적용했다.

  • 도메인 폴더: Content/Furniture/<종류>/
  • 접두사: SM_(스태틱메시) / MI_(머티리얼 인스턴스) / T_(텍스처)
  • 펭귄은 BP_Furniture_Penguin(ATCCarriableFurniture 상속)으로 운반 가능 가구화, 박스는 단순 충돌(box collision) 추가.
1
2
3
4
5
6
task = unreal.AssetImportTask()
task.filename = r"D:\...\refrigerator.glb"
task.destination_path = "/Game/Furniture/Refrigerator"
task.automated = True
task.save = True
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])

4. 레벨 라이팅 디버깅 — Rotator 인자 순서 함정

새 테스트 레벨이 깜깜했다. 원인은 DirectionalLight(태양)가 위를 향하고 있던 것 — 즉 빛이 하늘로 쏘여서 지면이 밤이 됐다.

범인은 파이썬 unreal.Rotator인자 순서였다.

1
2
3
4
5
# 의도: pitch -45 (태양을 비스듬히 아래로)
unreal.Rotator(-45, 45, 0)   # ❌ Rotator(roll, pitch, yaw) → pitch=+45 (태양이 지평선 아래!)

# 올바름
unreal.Rotator(0.0, -45.0, 45.0)  # roll=0, pitch=-45, yaw=45

unreal.Rotator의 생성자 인자는 (Roll, Pitch, Yaw) 순이다. 보통 머릿속으로 “pitch부터”라고 생각하기 쉬워서 첫 인자에 pitch 값을 넣는 실수를 했다. pitch가 +45가 되면서 태양이 지평선 아래로 들어가 밤이 된 것.

해결: pitch를 -45로 수정 + SkyLight(RealTimeCapture)SkyAtmosphere를 추가해 환경광·하늘을 살렸다.

5. 펭귄 스켈레탈 메시 임포트 — 크기·머티리얼 문제

penguin.fbx는 리깅된 스켈레탈 메시(78본, Character Creator의 cc_base_* 리그) + 애니메이션을 포함한다.

크기 함정 (1cm)

FBX가 단위 문제로 높이 1cm로 들어왔다. 재임포트 시 스케일을 100배로 강제했다.

1
2
opts = unreal.FbxImportUI()
opts.skeletal_mesh_import_data.import_uniform_scale = 100.0   # ~100cm로 보정

이후 플레이어 키(176cm)에 맞춰 메시 스케일 1.769 + 캡슐을 조정했다.

머티리얼 톤 (퐁 광택)

FBX가 FBXLegacyPhongSurfaceMaterial(퐁) 기반이라 Shininess=100으로 들어와 메탈릭처럼 번들거렸다(SkyLight 반사로 반짝임). Shininess를 2.0으로 낮춰 매트하게 보정했다.

6. VibeUE 파이썬의 두 가지 크래시 함정

에디터 안에서 파이썬을 돌리는 만큼, 잘못하면 에디터 자체가 죽는다. 두 패턴을 확실히 학습했다.

(1) CDO 직접 수정 금지

unreal.get_default_object(bp.generated_class())로 얻은 CDO(클래스 기본 객체)나 그 컴포넌트set_editor_property를 하면 에디터가 크래시한다. VibeUE가 명시적으로 막는 위험 패턴인데, 여러 줄로 쪼개 쓰면 안전장치를 우회해서 그대로 터진다.

1
2
3
4
5
6
7
8
# ❌ 위험: CDO/컴포넌트 직접 수정 → 크래시
cdo = unreal.get_default_object(bp.generated_class())
comp = cdo.get_editor_property("mesh_component")
comp.set_editor_property("skeletal_mesh", mesh)   # 여기서 크래시

# ✅ 안전: SubobjectDataSubsystem으로 BP 컴포넌트 "템플릿"을 수정
sds = unreal.get_engine_subsystem(unreal.SubobjectDataSubsystem)
# 핸들을 통해 컴포넌트 템플릿에 프로퍼티 적용

블루프린트의 컴포넌트를 바꾸려면 CDO가 아니라 SubobjectDataSubsystem 으로 서브오브젝트 핸들을 얻어 템플릿을 수정해야 한다.

(2) 레벨 빠른 전환/삭제 금지

한 호출(한 파이썬 실행) 안에서 load_level / new_level / 열린 레벨 delete연속으로 하면 "World Memory Leaks" fatal 크래시가 난다. 월드가 정리되기 전에 다음 작업이 들어가서 누수가 검출되는 것.

레벨 이동은 호출을 나눠서: 다른 레벨로 전환 → duplicate_assetsave → (별도 호출에서) 원본 delete.

7. BP_PenguinCharacter 제작 + PIE 크래시(PC null)

BP_PlayerCharacter(ATCPlayerCharacter 상속 — 이동/입력/잡기/카메라 내장)를 복제해 메시만 펭귄으로 교체했다. 코드 자산을 그대로 재사용하는 방식.

누운 문제

메시 회전을 pitch -90으로 잘못 넣어 펭귄이 누웠다. yaw -90이어야 한다 — 또 Rotator 인자순서(roll, pitch, yaw) 문제였다.

PIE 크래시 (PC null → Access Violation)

캐릭터를 레벨에 직접 배치하고 auto_possess_player=Player0로 테스트하니 TCPlayerCharacter::BeginPlayPC->GetLocalPlayer()에서 PC(PlayerController)가 null이라 Access Violation으로 죽었다.

원인 두 가지:

  1. checkf 가드가 이 빌드 구성에서 컴파일 제외되어 null이 그대로 통과.
  2. 배치형 폰의 빙의(possess) 타이밍이 어긋나, BeginPlay 시점에 아직 컨트롤러가 붙지 않음.

해결: 배치형 auto-possess를 쓰지 말고, GameMode의 DefaultPawnClass로 지정해 정상 스폰·빙의 경로를 타게 했다.

1
2
배치형 폰 + auto_possess_player=Player0   → BeginPlay 시 PC null (빙의 미완료)
GameMode.DefaultPawnClass = BP_PenguinCharacter → 스폰 후 빙의 보장 ✅

이를 위해 네트워크용 프레임워크도 만들었다.

  • BP_NetTestGameModeBase(BP_TestGameModeBase 상속) — DefaultPawn=펭귄, GameStateClass=BP_NetTestGameStateBase
  • 레벨의 World Settings → GameMode Override로 타게팅

8. VRAM 고갈 — 8K 텍스처 미렌더

PIE에서 펭귄이 안 보이고 "Video memory exhausted" 경고가 떴다. 원인: 임포트된 텍스처 6개가 전부 8192×8192(8K) 라 VRAM이 폭발해 렌더 리소스가 로드되지 못한 것(메시는 있는데 그릴 텍스처를 못 올림).

해결: 모든 텍스처에 max_texture_size=1024 캡을 적용.

1
2
3
tex = unreal.EditorAssetLibrary.load_asset(tex_path)
tex.set_editor_property("max_texture_size", 1024)   # 8K → 1K 캡
unreal.EditorAssetLibrary.save_asset(tex_path)

8K 텍스처 6장이면 비압축 기준만 해도 수 GB라 캐릭터 한 마리에 과하다. 외부(Tripo/CC) 에셋은 텍스처가 8K로 들어올 수 있으니 임포트 직후 캡을 확인하는 습관이 필요하다.

9. 애니메이션 리타게팅 — 가장 긴 삽질

펭귄에 이동 애니메이션을 붙이는 게 이날 가장 오래 걸린 작업이었다.

시도1 — IK Retargeter (cross-rig)

플레이어(33본 스타일라이즈드 리그) → 펭귄(78본 CC 리그)은 공통 본 이름이 0개였다. IK Rig 2개 + IK Retargeter(체인 매핑, auto_align)를 자동 구성하고 배치 리타게팅까지 돌렸지만, 두 리그가 너무 달라 포즈가 붕괴(메시가 찌그러짐)했다.

  • auto_align_all_bones가 타깃 포즈를 망가뜨렸다.
  • retarget pose를 리셋해도 한계. 본 구조가 근본적으로 다르면 cross-rig 리타게팅은 깨진다는 걸 체감.

시도2 — Mixamo

  • Mixamo는 머티리얼/텍스처를 안 보여줘서 청록 단색으로 표시(정상 동작).
  • “Without Skin”(애님만) FBX는 메시가 없어 임포트 실패 → “With Skin” 이 필요.
  • 다행히 기존 SK_Penguin_Skeleton(cc_base 본)과 Mixamo 애님 본 이름이 일치(78본)해서 임포트는 됐다.

진짜 원인 = 스케일 미스매치

Mixamo 애님을 SK_Penguin_Skeleton에 올리니 메시가 1cm로 압착됐다. SK_Penguinimport_uniform_scale=100으로 들어와 스켈레톤이 100배인데, 애님은 1배라 1/100로 쪼그라든 것.

1
2
# 애님도 메시와 "동일한" 임포트 스케일로 맞춰야 한다
opts.anim_sequence_import_data.import_uniform_scale = 100.0   # 키 ~106cm로 정상

외부 애님은 반드시 메시 임포트 스케일과 동일 스케일로 임포트해야 한다.

ABP / BlendSpace 구성

ABP_PlayerCharacter를 IK Retarget 배치로 복제해 그래프 구조(Speed 1D BlendSpace: 0=idle, 250=walk, 500=run)를 가져오고, BlendSpace 샘플 애님을 펭귄의 비붕괴 애님(Breathing_Idle / Walking)으로 교체했다. BP_PenguinCharacterABP_Penguin을 적용.

루트모션 드리프트

enable_root_motion=False로 둬도 메시가 앞으로 갔다 되돌아오는 드리프트가 있었다. 측정해보니 전진 이동이 root 본의 translation 트랙에 baked되어 있었다(root world Y가 0→71로 변함).

해결: SK_Penguin_Skeletonroot 본 translation 리타게팅 모드를 Skeleton으로 설정.

1
2
3
skel = unreal.load_asset("/Game/.../SK_Penguin_Skeleton")
# root 본만 translation을 스켈레톤(레퍼런스) 기준으로 → 애님의 전진 이동 무시(제자리)
SkeletonService.set_bone_retargeting_mode(skel, "root", "Skeleton")

이렇게 하면 애님의 전진 트랜슬레이션을 무시하고 레퍼런스 포즈 위치를 써서 root Y가 0으로 고정 → 드리프트 제거(in-place). 단, root 본만 바꿔야 한다(전체 본을 Skeleton으로 하면 다시 붕괴).

10. 오늘 배운 것 정리

  1. VibeUE = 2단계 MCP + 에디터 파이썬. list_toolsets → describe_toolset → call_tool 또는 execute_python_code. 개인 도구는 skip-worktree(추적 파일) + .git/info/exclude(미추적)로 팀에 안 새게 격리한다.
  2. 에디터 파이썬 자동화의 두 크래시. CDO/컴포넌트 직접 set_editor_property는 크래시(여러 줄로 쪼개도 위험) → SubobjectDataSubsystem. 레벨 load/new/delete 연속도 크래시 → 호출 분리.
  3. 임포트 스케일은 메시·애님을 반드시 일치. FBX가 1cm로 들어오면 import_uniform_scale=100. 그런데 애님도 같은 스케일로 안 넣으면 메시가 1/100로 압착된다.
  4. Rotator 인자 순서는 (roll, pitch, yaw). 라이팅 어둠(태양 pitch 부호)도, 펭귄 누움(yaw 자리에 pitch)도 전부 이 순서를 헷갈려서 났다.
  5. 외부 에셋 텍스처는 8K로 들어올 수 있다. VRAM 고갈로 메시가 아예 안 그려질 수 있으니 max_texture_size로 캡.
  6. PIE 캐릭터 테스트는 DefaultPawnClass로. 배치형 auto-possess는 빙의 타이밍이 어긋나 BeginPlay에서 PC null → Access Violation.
  7. cross-rig 리타게팅의 한계 vs in-place. 본 구조가 다르면 IK Retargeter는 붕괴. 본 이름이 같으면 동일 스켈레톤으로 임포트하고, 전진은 root 본만 Skeleton 리타게팅으로 제자리 처리한다.
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.