TIL - 커스텀 플러그인 만들기

Kyu_·2026년 6월 23일

31주차

목록 보기
3/6

NeoSanctum 커스텀 플러그인 정리


1. PartIconTool

왜 만들었나

파츠 줍기 프롬프트 위젯(드롭 파츠 Radius 진입 시 표시)에 파츠 이미지와 이름을 띄우려 했다.
파츠 이미지 소스로 콘텐츠 브라우저의 스켈레탈 메시 썸네일을 활용하고 싶었는데, 썸네일 캡처 API(ThumbnailTools, FObjectThumbnail)가 에디터 전용 모듈(UnrealEd) 에만 존재한다.
런타임 게임 모듈에서는 접근 자체가 불가하고, 패키징(쿠킹) 시 썸네일 데이터도 제거된다.

해결 방향은 두 가지였다:

  • 방법 A — 런타임에 SceneCapture로 메시를 실시간 렌더해 RenderTarget을 아이콘으로 사용 → 드롭 프롬프트처럼 자주 뜨고 사라지는 UI에는 메모리/성능 부담이 큼
  • 방법 B — 에디터에서 썸네일을 텍스처 애셋으로 1회 추출, UNSPartDefinition::Icon에 저장 → 런타임 비용 0, 기존 소프트 레퍼런스 비동기 로드 구조와 자연스럽게 연결됨

방법 B를 선택했고, 이 작업을 에디터 플러그인으로 구현했다.


왜 플러그인으로 만들었나

  • 썸네일 캡처는 에디터 전용 기능이라 게임 빌드(Shipping)에 포함되면 안 됨
  • 에디터 모듈을 게임 모듈 안에 넣으면 구조가 지저분해지고 유지보수가 어려움
  • 플러그인(Type: Editor)은 에디터 빌드에만 포함되고 패키징 시 자동으로 제외됨
  • 나중에 다른 프로젝트로 이식하거나 팀원과 공유하기 좋음

구성

Plugins/PartIconTool/
├── PartIconTool.uplugin                 (Editor 타입, LoadingPhase: PostEngineInit)
└── Source/PartIconTool/
    ├── PartIconTool.Build.cs
    ├── Public/
    │   ├── PartIconTool.h               (모듈 클래스 선언)
    │   └── PartIconToolSettings.h       (UDeveloperSettings — 캡처 파라미터)
    └── Private/
        └── PartIconTool.cpp             (2패스 SceneCapture + 메뉴 등록)

의존 모듈

모듈용도
UnrealEdFPreviewScene, 에디터 유틸리티
ToolMenus콘텐츠 브라우저 우클릭 메뉴 확장
ContentBrowserUContentBrowserAssetContextMenuContext
AssetRegistryAssetCreated 알림 (콘텐츠 브라우저 즉시 반영)
RenderCoreFlushRenderingCommands
DeveloperSettingsUPartIconToolSettings 베이스 클래스
NeoSanctumUNSPartDefinition 타입 직접 참조

동작 흐름

  1. 콘텐츠 브라우저에서 UNSPartDefinition 에셋 1개 이상 선택 → 우클릭
  2. "Generate Icon from Mesh" 클릭 (선택 항목 중 파츠 Definition이 없으면 메뉴 비노출)
  3. GetDefault<UPartIconToolSettings>()으로 캡처 파라미터 읽기 (해상도/각도/밝기)
  4. 각 Definition에 대해 PartMesh.LoadSynchronous()로 스켈레탈 메시 로드
  5. Mesh->WaitForPendingInitOrStreaming() + FlushRenderingCommands() — 렌더 리소스 준비 대기
  6. FPreviewScene 생성 — 격리된 씬에 메시 + 라이트 + USceneCaptureComponent2D 배치
  7. GShaderCompilingManager->FinishAllCompilation() — 머티리얼 셰이더 컴파일 완료 대기
  8. 1패스 (SCS_FinalColorLDR) — 색상 BGRA8 캡처
  9. 2패스 (SCS_SceneColorHDR) — 커버리지 마스크 캡처, 1 - alpha로 누끼 알파 생성
  10. 두 패스 픽셀 합성 후 UTexture2D 애셋 생성
    • 저장 경로: Definition과 같은 폴더, 이름 T_<DefName>_Icon
    • 설정: TC_EditorIcon / TEXTUREGROUP_UI / NeverStream = true / SRGB = true
    • 같은 이름 애셋이 이미 있으면 덮어쓰기(재생성)
  11. 생성된 텍스처를 Def->Icon에 지정
  12. 텍스처 패키지 + Definition 패키지 둘 다 디스크 저장
  13. 성공/건너뜀 개수 알림 팝업 표시

PartMesh가 비어 있는 Definition은 경고 로그 출력 후 건너뜀.

캡처 파라미터 조절: Project Settings > Plugins > Part Icon Tool에서 빌드 없이 조절 가능.

설정기본값설명
IconResolution256캡처 해상도 (px)
OrbitPitch-11.25카메라 상하 각도
OrbitYaw-157.5카메라 좌우 각도
OrbitZoom0줌 오프셋
DistanceMultiplier1.15메시 여백 배율
LightBrightness1.0정면 라이트 밝기

코드 진화 과정

요구사항 시작점

파츠 줍기 프롬프트에 아이콘을 보여주기로 했고, 그 아이콘을 스켈레탈 메시 썸네일처럼 찍어서 쓰고 싶다는 요구였다. 처음엔 ThumbnailTools로 썸네일을 그대로 읽어 텍스처로 저장하는 구조로 시작했다.

1세대 — ThumbnailTools 기반

GenerateThumbnailForObjectToSaveToDisk(Mesh)
→ FObjectThumbnail → 픽셀 추출 → UTexture2D 저장

문제: 처음 캡처된 이미지가 완전히 검정이었다. 렌더 리소스가 GPU에 올라가기 전에 캡처되었기 때문. WaitForPendingInitOrStreaming() + FlushRenderingCommands()를 추가했지만 여전히 검정이 나왔다. ThumbnailTools 자체가 에디터 뷰포트 상태에 의존적이라 격리된 환경에서 안정적으로 동작하지 않는다는 결론.

2세대 — SceneCapture + FPreviewScene 기반 (현재)

썸네일 렌더러에 의존하지 않고 USceneCaptureComponent2D로 직접 메시를 찍는 구조로 전환했다. FPreviewScene으로 격리된 씬을 만들고 메시 컴포넌트와 라이트, 카메라를 직접 배치한다.

FPreviewScene
  └── USkeletalMeshComponent (파츠 메시)
  └── USceneCaptureComponent2D
        → UTextureRenderTarget2D → 픽셀 읽기 → UTexture2D 저장

이 구조로 전환하자 검정 문제가 사라졌다.

누끼(배경 투명화) 요구

아이콘을 위젯 배경에 자연스럽게 녹이고 싶다는 요구가 추가됐다. SCS_FinalColorLDR은 렌더 안 된 영역도 alpha=1(불투명)로 채워서 검정 배경이 생긴다. 해결을 위해 2패스 캡처로 전환:

  • 1패스 (SCS_FinalColorLDR) — 색상 정확도 높은 BGRA8 캡처
  • 2패스 (SCS_SceneColorHDR) — alpha 채널에 커버리지 마스크 포함 (1 - alpha = 메시가 있는 곳)

두 패스 결과를 합쳐서 1패스 색상 + 2패스 커버리지를 alpha로 적용, 배경이 투명한 PNG형 텍스처 생성.

// 1패스: 색상 픽셀 읽기
Capture->CaptureSource = SCS_FinalColorLDR;
Capture->CaptureScene();
RTResource->ReadPixels(Pixels, ...);

// 2패스: 알파(커버리지) 픽셀 읽기
Capture->CaptureSource = SCS_SceneColorHDR;
Capture->CaptureScene();
MaskResource->ReadLinearColorPixels(MaskPixels);

// 합성
for (int32 i = 0; i < Pixels.Num(); ++i)
{
    const float Coverage = FMath::Clamp(1.f - MaskPixels[i].A, 0.f, 1.f);
    Pixels[i].A = static_cast<uint8>(FMath::RoundToInt(Coverage * 255.f));
}

머티리얼 미로드 문제

2세대로 전환 후 메시가 렌더되긴 하는데 머티리얼이 모두 흰색(기본 머티리얼)으로 나오는 문제가 생겼다. 원인은 머티리얼 셰이더가 아직 컴파일 중인 상태에서 캡처했기 때문. GShaderCompilingManager->FinishAllCompilation() 호출로 해결.

과노출 문제

라이트 밝기(LightBrightness=4.0) + 자동 노출(AutoExposure=1.0)이 겹쳐서 메시 전체가 흰색으로 날아가는 문제가 발생했다. LightBrightness를 1.0으로 낮추고 ShowFlags.SetBloom(false)로 블룸 번짐을 차단했다.

캡처 설정 조정 요구 → UDeveloperSettings 도입

각도나 밝기를 코드 수정 없이 바꿀 수 있게 해달라는 요구. UDeveloperSettings(config=Editor)를 상속한 UPartIconToolSettings 클래스를 추가해 Project Settings > Plugins > Part Icon Tool 에서 직접 조절할 수 있게 했다. 빌드 없이 설정 변경 후 바로 Generate하면 즉시 반영된다.


최종 구조

Plugins/PartIconTool/
├── PartIconTool.uplugin
└── Source/PartIconTool/
    ├── PartIconTool.Build.cs
    ├── Public/
    │   ├── PartIconTool.h
    │   └── PartIconToolSettings.h       (UDeveloperSettings — 캡처 파라미터)
    └── Private/
        └── PartIconTool.cpp             (2패스 SceneCapture + 메뉴 등록)

캡처 파이프라인 요약

[콘텐츠 브라우저 우클릭 → Generate Icon from Mesh]
  ↓
PartMesh.LoadSynchronous() + WaitForPendingInitOrStreaming()
  ↓
FPreviewScene 생성 (메시 + 라이트 + SceneCaptureComponent2D)
  ↓
GShaderCompilingManager->FinishAllCompilation()   ← 셰이더 준비 대기
  ↓
1패스: SCS_FinalColorLDR → Pixels (색상)
2패스: SCS_SceneColorHDR → MaskPixels (커버리지)
  ↓
Pixels[i].A = 1 - MaskPixels[i].A              ← 누끼 합성
  ↓
UTexture2D 생성/갱신 (T_<DefName>_Icon, TC_EditorIcon)
  ↓
Def->Icon = 생성된 텍스처, 패키지 저장

의존 모듈

모듈용도
UnrealEdFPreviewScene, 에디터 유틸리티
ToolMenus콘텐츠 브라우저 우클릭 메뉴 확장
ContentBrowserUContentBrowserAssetContextMenuContext
AssetRegistryAssetCreated 알림 (콘텐츠 브라우저 즉시 반영)
RenderCoreFlushRenderingCommands
DeveloperSettingsUPartIconToolSettings 베이스 클래스
NeoSanctumUNSPartDefinition 타입 직접 참조

있었던 문제들 (요약)

#증상원인해결
1캡처 결과가 완전 검정ThumbnailTools 에디터 뷰포트 의존성, 리소스 미준비SceneCapture + FPreviewScene으로 전환
2FlushRenderingCommands LNK2019Build.csRenderCore 누락PrivateDependencyModuleNames에 추가
3머티리얼이 흰색 기본 머티리얼로 렌더됨캡처 시 셰이더 아직 컴파일 중GShaderCompilingManager->FinishAllCompilation() 추가
4메시 전체가 흰색으로 날아감 (과노출)LightBrightness=4.0 + 자동 노출 중첩LightBrightness=1.0 + SetBloom(false)
5배경이 검정으로 고정됨 (누끼 안 됨)SCS_FinalColorLDR은 alpha 항상 불투명2패스 캡처: 2패스 alpha로 커버리지 마스크 생성 후 합성

사용 방법

  1. 파츠 Definition 에셋(UNSPartDefinition)을 콘텐츠 브라우저에서 선택
  2. 우클릭 → Generate Icon from Mesh
  3. 같은 폴더에 T_<DefName>_Icon 텍스처가 생성되고 Icon 필드에 자동 지정됨
  4. 이후 드롭 파츠 Radius 진입 시 프롬프트에 파츠 이름 + 아이콘 표시

각도나 밝기를 조정하려면 Project Settings > Plugins > Part Icon Tool 에서 수치를 바꾼 뒤 다시 Generate하면 된다. 빌드 불필요.

파츠를 추가할 때마다 이 작업을 1회 실행해야 아이콘이 갱신된다.

0개의 댓글