[TIL] 2026-06-24 — VibeUE(언리얼 MCP)로 펭귄 캐릭터 임포트 · 스케일·크래시·리타게팅 삽질기
이날의 큰 줄기는 VibeUE(언리얼 MCP) 에이전트를 설치하고, 그걸로 펭귄 캐릭터를 임포트→셋업→애니메이션까지 붙이는 과정이었다. 자연어로 에디터를 조작하는 신세계였지만, 그만큼 파이썬 자동화의 함정(CDO 직접 수정 크래시, 레벨 빠른전환 크래시), FBX/GLB 임포트 스케일 미스매치, 8K 텍스처로 인한 VRAM 고갈, 그리고 cross-rig 리타게팅의 한계까지 — 함정을 하나씩 밟으며 원인과 해법을 정리했다. 곁들여 협동 운반 멀티플레이의
ATCCarriableFurnitureC++ 베이스도 작성했다.
오늘 한 일 요약
- VibeUE(언리얼 MCP) 플러그인 설치 + 2단계 MCP 도구 구조 파악 + 팀에 안 새는 로컬 전용 git 설정
- 네트워크 C++ 토대 빌드 검증(리슨서버 PIE) + 협동 운반 베이스
ATCCarriableFurniture작성 - Tripo GLB 6종 임포트 + 팀 컨벤션(도메인 폴더·접두사) 적용
- 새 레벨이 어두운 원인 = DirectionalLight pitch 부호 → Rotator 인자순서(roll,pitch,yaw) 함정 규명
- 펭귄 FBX(78본 CC 리그) 임포트 — 1cm 크기 함정 + 퐁 머티리얼 광택 보정
- 파이썬 자동화의 CDO 직접 수정 / 레벨 빠른전환 크래시 안전 패턴 정립
BP_PenguinCharacter제작 + PIE PC null 크래시 → DefaultPawnClass 스폰 경로로 해결- 8K 텍스처 VRAM 고갈 →
max_texture_size=1024캡 - 애니메이션 리타게팅 대장정 — 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_toolsets→describe_toolset→call_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_asset→save→ (별도 호출에서) 원본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::BeginPlay의 PC->GetLocalPlayer()에서 PC(PlayerController)가 null이라 Access Violation으로 죽었다.
원인 두 가지:
checkf가드가 이 빌드 구성에서 컴파일 제외되어 null이 그대로 통과.- 배치형 폰의 빙의(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_Penguin이 import_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_PenguinCharacter에 ABP_Penguin을 적용.
루트모션 드리프트
enable_root_motion=False로 둬도 메시가 앞으로 갔다 되돌아오는 드리프트가 있었다. 측정해보니 전진 이동이 root 본의 translation 트랙에 baked되어 있었다(root world Y가 0→71로 변함).
해결: SK_Penguin_Skeleton의 root 본 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. 오늘 배운 것 정리
- VibeUE = 2단계 MCP + 에디터 파이썬.
list_toolsets → describe_toolset → call_tool또는execute_python_code. 개인 도구는skip-worktree(추적 파일) +.git/info/exclude(미추적)로 팀에 안 새게 격리한다. - 에디터 파이썬 자동화의 두 크래시. CDO/컴포넌트 직접
set_editor_property는 크래시(여러 줄로 쪼개도 위험) →SubobjectDataSubsystem. 레벨 load/new/delete 연속도 크래시 → 호출 분리. - 임포트 스케일은 메시·애님을 반드시 일치. FBX가 1cm로 들어오면
import_uniform_scale=100. 그런데 애님도 같은 스케일로 안 넣으면 메시가 1/100로 압착된다. - Rotator 인자 순서는 (roll, pitch, yaw). 라이팅 어둠(태양 pitch 부호)도, 펭귄 누움(yaw 자리에 pitch)도 전부 이 순서를 헷갈려서 났다.
- 외부 에셋 텍스처는 8K로 들어올 수 있다. VRAM 고갈로 메시가 아예 안 그려질 수 있으니
max_texture_size로 캡. - PIE 캐릭터 테스트는 DefaultPawnClass로. 배치형 auto-possess는 빙의 타이밍이 어긋나 BeginPlay에서 PC null → Access Violation.
- cross-rig 리타게팅의 한계 vs in-place. 본 구조가 다르면 IK Retargeter는 붕괴. 본 이름이 같으면 동일 스켈레톤으로 임포트하고, 전진은 root 본만 Skeleton 리타게팅으로 제자리 처리한다.