포스트

[TIL] 2026-06-24 — 펭귄 캐릭터 임포트 트러블슈팅: 스케일·크래시·리타게팅

펭귄 캐릭터를 에디터에 임포트하며 겪은 스케일·머티리얼·PIE 크래시·VRAM 고갈·애니메이션 리타게팅 연쇄 트러블슈팅 기록.

[TIL] 2026-06-24 — 펭귄 캐릭터 임포트 트러블슈팅: 스케일·크래시·리타게팅

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

오늘 한 일 요약

  1. 에디터 파이썬 자동화 도구 설치 + 도구 구조 파악 + 팀에 안 새는 로컬 전용 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. 에디터 파이썬 자동화 설정 + 로컬 전용 git 설정

에디터 파이썬 자동화 도구

UE 에디터를 외부에서 파이썬으로 조작하는 자동화 플러그인을 프로젝트 Plugins/에 git clone 했다. UE 5.8용으로, 블루프린트·머티리얼·애니메이션·지형·위젯·성능 프로파일링 등 30+ 서비스를 제공한다. 핵심은 에디터 프로세스 안에서 파이썬/툴 호출로 에디터를 조작한다는 것 — execute_python_code에디터 프로세스 안에서 직접 파이썬을 실행할 수 있다.

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

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

이 자동화 플러그인은 개인 도구라 팀 저장소에 커밋되면 안 된다. 추적 여부에 따라 두 방법을 나눠 썼다.

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

# 미추적 항목은 로컬 전용 exclude로 (팀에 공유되는 .gitignore 대신)
printf "/<도구 설정>.json\n/Plugins/<도구>/\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(Play In Editor, 에디터 안에서 바로 실행하는 테스트 플레이) 세션 진입까지 확인했다.

이어서 운반 가구의 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. 에디터 파이썬 자동화의 두 가지 크래시 함정

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

(1) CDO 직접 수정 금지

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

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. 에디터 파이썬 자동화. 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 리타게팅으로 제자리 처리한다.

오늘 배운 것 — 외부 FBX/GLB 파이프라인의 함정은 대부분 “스케일·좌표 규약 불일치”였다. 메시와 애니메이션의 임포트 스케일을 반드시 같게 맞춰야 하고, Rotator 인자 순서(roll, pitch, yaw)를 헷갈리면 라이팅 어둠부터 캐릭터 눕는 현상까지 전부 여기서 난다. 에디터 파이썬 자동화에서는 CDO 직접 수정과 레벨 연속 전환이 에디터 크래시를 부른다는 것도 몸으로 확인했다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.