반응형
🎮 구현 목표
Unity에서 제작된 스프라이트 시트의 메타데이터(.smeta) 파일을 파싱하여, DirectX 11 엔진에서 개별 스프라이트의 UV 좌표와 크기 정보를 자동으로 추출하고 관리하는 시스템을 구축합니다.
🚨 문제 상황
- 반복적인 수작업의 비효율성
- 리소스를 포토샵에서 하나 하나 크기에 맞춰 잘라야하는 수작업
- 리소스를 분석하여 프레임 순서에 맞춰 파일 이름을 정렬
- 스프라이트 시트에서 각 프레임의 픽셀 좌표와 크기를 일일이 계산
- 프로그램에서 렌더링 후 피봇 불일치 시, 다시 포토샵으로 작업 후 업로드하는 불편함
- 유지보수의 어려움
- 스프라이트 인덱스를 직접 계산하고 체크 (위험)
사전에 리소스를 준비하고 편집하는 시간이 상당히 오래걸리고, 리소스를 추가할 때마다 수 시간이 소요되는 작업 부하가 발생했습니다.
리스소와 관련된 작업의 부담을 해소하기 위해 파일시스템과 데이터 파싱을 이용하기로 했습니다.
💭 해결 방안
아키텍처
UEngineDirectory (파일 탐색)
↓
UEngineFile (파일 읽기)
↓
UEngineSprite::CreateSpriteToMeta (메타 파싱)
↓
FSpriteData (UV 좌표 변환)
↓
SpriteRenderer (렌더링)
1. 파일 시스템 추상화
class UEngineDirectory : public UEnginePath
{
public:
// 확장자 필터링과 재귀 탐색을 지원하는 파일 검색
std::vector<UEngineFile> GetAllFile(
bool _IsRecursive,
const std::vector<std::string>& _Exts
);
std::vector<UEngineDirectory> GetAllDirectory();
};
설계 의도:
- 디렉토리 구조를 추상화하여 리소스 자동 로딩 시스템 구축
- 확장자 기반 필터링으로 원하는 파일만 선별적으로 처리
- 재귀 탐색으로 서브 디렉토리 리소스까지 일괄 로딩
2. Unity 메타데이터 포맷 분석

Unity .smeta 파일 구조:
rect:
serializedVersion: 2
x: 128
y: 256
width: 64
height: 64
alignment: 0
pivot: {x: 0.5, y: 0.5}
outline: []
핵심 정보:
rect: 스프라이트 시트 내 픽셀 좌표와 크기pivot: 스프라이트의 중심점 (0~1 정규화 값)
3. 문자열 파싱 전략
//"rect:"와 "outline:" 사이의 텍스트 추출
size_t RectIndex = Text.find("rect:", StartPosition);
size_t AligIndex = Text.find("outline:", RectIndex);
if (RectIndex == std::string::npos || AligIndex == std::string::npos)
{
break; // 더 이상 스프라이트 데이터가 없음
}
NewSpirte->SpriteTexture.push_back(Tex.get());
SpriteDataTexts.push_back(Text.substr(RectIndex, AligIndex - RectIndex));
StartPosition = AligIndex;
파싱 로직:
rect:키워드로 스프라이트 데이터 시작 지점 탐색outline:키워드로 종료 지점 확인- 두 키워드 사이의 텍스트 블록을 추출하여 개별 스프라이트 데이터로 분리
4. 수치 데이터 추출
FSpriteData SpriteData;
// X 좌표 추출
std::string Number = UEngineString::InterString(Text, "x:", "\n", Start);
SpriteData.CuttingPos.X = static_cast<float>(atof(Number.c_str()));
// Y 좌표 추출
Number = UEngineString::InterString(Text, "y:", "\n", Start);
SpriteData.CuttingPos.Y = static_cast<float>(atof(Number.c_str()));
// Width 추출
Number = UEngineString::InterString(Text, "width:", "\n", Start);
SpriteData.CuttingSize.X = static_cast<float>(atof(Number.c_str()));
// Height 추출
Number = UEngineString::InterString(Text, "height:", "\n", Start);
SpriteData.CuttingSize.Y = static_cast<float>(atof(Number.c_str()));
// Pivot X 추출 (쉼표로 구분)
Number = UEngineString::InterString(Text, "x:", ",", Start);
SpriteData.Pivot.X = static_cast<float>(atof(Number.c_str()));
// Pivot Y 추출 (중괄호로 종료)
Number = UEngineString::InterString(Text, "y:", "}", Start);
SpriteData.Pivot.Y = static_cast<float>(atof(Number.c_str()));
InterString 함수의 역할:
- 시작 문자열과 종료 문자열 사이의 부분을 추출
Start변수로 파싱 위치를 추적하여 순차적으로 값을 읽음
5. 좌표계 변환 및 정규화
FVector TexSize = Tex->GetTextureSize();
// Y축 반전 (Unity는 왼쪽 하단 기준, DirectX는 왼쪽 상단 기준)
SpriteData.CuttingPos.Y = TexSize.Y - SpriteData.CuttingPos.Y - SpriteData.CuttingSize.Y;
// 픽셀 좌표를 0~1 UV 좌표로 정규화
SpriteData.CuttingPos.X /= TexSize.X;
SpriteData.CuttingPos.Y /= TexSize.Y;
SpriteData.CuttingSize.X /= TexSize.X;
SpriteData.CuttingSize.Y /= TexSize.Y;
좌표계 차이 해결:
| 엔진 | 원점 위치 | Y축 방향 |
|------|----------|---------|
| Unity | 왼쪽 하단 | 위쪽이 양수 |
| DirectX | 왼쪽 상단 | 아래쪽이 양수 |
정규화의 이점:
- 텍스처 크기와 무관하게 동일한 UV 좌표 사용 가능
- GPU 샘플러가 직접 활용할 수 있는 형식
🔧 시행착오
1. 초기 구현: 폴더 기반 스프라이트 로딩
std::shared_ptr<UEngineSprite> UEngineSprite::CreateSpriteToFolder(
std::string_view _Name,
std::string_view _Path)
{
UEngineDirectory Dir = _Path;
std::vector<UEngineFile> Files = Dir.GetAllFile(false, { ".png"});
for (size_t i = 0; i < Files.size(); i++)
{
// 기본값으로 전체 텍스처 사용 (UV: 0~1)
FSpriteData SpriteData;
SpriteData.CuttingPos = { 0.0f, 0.0f }; // 시작점
SpriteData.CuttingSize = { 1.0f, 1.0f }; // 전체 크기
SpriteData.Pivot = { 0.5f, 0.5f }; // 중앙 피벗
NewSpirte->SpriteDatas.push_back(SpriteData);
}
}
한계점:
- 스프라이트 시트를 활용할 수 없고, 개별 이미지 파일만 처리 가능
- 애니메이션 프레임마다 별도 파일이 필요해 파일 관리 부담 증가
- 텍스처 전환 오버헤드로 렌더링 성능 저하
2. 문자열 파싱 중 인덱스 관리 문제
초기 시도:
// 잘못된 예시 - Start 변수를 갱신하지 않음
std::string Number = UEngineString::InterString(Text, "x:", "\n", 0);
SpriteData.CuttingPos.X = atof(Number.c_str());
// 동일한 시작 위치에서 다시 검색하면 첫 번째 "y:"를 찾게 됨
Number = UEngineString::InterString(Text, "y:", "\n", 0); // 버그!
해결책:
// Start 변수를 참조로 전달하여 파싱 위치 추적
size_t Start = 0; // 파싱 커서
// InterString 함수가 Start를 자동으로 갱신
std::string Number = UEngineString::InterString(Text, "x:", "\n", Start);
SpriteData.CuttingPos.X = atof(Number.c_str());
// 다음 호출 시 이전 위치 다음부터 검색
Number = UEngineString::InterString(Text, "y:", "\n", Start);
SpriteData.CuttingPos.Y = atof(Number.c_str());
3. Y축 좌표계 변환 누락
문제 상황:
// 변환 없이 그대로 사용 시
SpriteData.CuttingPos.Y = PixelY / TexSize.Y;
// 결과: 스프라이트가 상하 반전되어 렌더링됨
원인 분석:
- Unity: 원점이 왼쪽 하단, Y축이 위쪽으로 증가
- DirectX: 원점이 왼쪽 상단, Y축이 아래쪽으로 증가
해결:
// 135 라인: Y축 반전 공식
SpriteData.CuttingPos.Y = TexSize.Y - SpriteData.CuttingPos.Y - SpriteData.CuttingSize.Y;
이 공식은 다음과 같이 동작합니다:
TexSize.Y: 텍스처 전체 높이CuttingPos.Y: Unity 좌표계의 Y 위치CuttingSize.Y: 스프라이트 높이- 결과: DirectX 좌표계로 변환된 Y 위치
4. 멀티 스프라이트 처리
초기 구조:
// 하나의 메타 파일에 하나의 스프라이트만 처리
std::shared_ptr<UEngineSprite> CreateSpriteToMeta(std::string_view _Name);
개선된 구조:
// 루프를 통한 멀티 스프라이트 처리
while (true)
{
size_t RectIndex = Text.find("rect:", StartPosition);
size_t AligIndex = Text.find("outline:", RectIndex);
if (RectIndex == std::string::npos || AligIndex == std::string::npos)
{
break; // 모든 스프라이트 추출 완료
}
NewSpirte->SpriteTexture.push_back(Tex.get());
SpriteDataTexts.push_back(Text.substr(RectIndex, AligIndex - RectIndex));
StartPosition = AligIndex; // 다음 검색 위치 갱신
}
개선 효과:
- 하나의 스프라이트 시트에 여러 프레임이 있어도 자동으로 모두 추출
- 애니메이션 시퀀스를 하나의 리소스로 통합 관리 가능
✅ 결과
1. 리소스 로딩 자동화
// ContentsResource.cpp: 메타데이터 기반 일괄 로딩
void UContentsResource::LoadSpriteMetaData()
{
UEngineSprite::CreateSpriteToMeta("WanderingHusk.png", ".smeta");
UEngineSprite::CreateSpriteToMeta("LeapingHusk.png", ".smeta");
UEngineSprite::CreateSpriteToMeta("FalseKnight.png", ".smeta");
UEngineSprite::CreateSpriteToMeta("Explode.png", ".smeta");
// 한 줄로 수십 개의 프레임 자동 로딩
}
2. 애니메이션 구현 간소화
// 사용 예시: 몬스터 애니메이션 설정
std::shared_ptr<UEngineSprite> WanderingHuskSprite =
UEngineSprite::Find<UEngineSprite>("WanderingHusk.png");
// 스프라이트 개수 자동 파악
int FrameCount = WanderingHuskSprite->GetSpriteCount();
// 프레임별 UV 좌표 자동 적용
for (int i = 0; i < FrameCount; ++i)
{
FSpriteData Data = WanderingHuskSprite->GetSpriteData(i);
// Data.CuttingPos, Data.CuttingSize를 셰이더에 전달
}
3. 성능 개선 효과
이전 방식: 개별 파일 방식
- 텍스처 교체: 프레임당 1회 (비용 높음)
- Draw Call: 프레임 수만큼 증가
- 메모리: 개별 텍스처들의 합
현재 방식: 스프라이트 시트
- 텍스처 교체: 0회 (동일 텍스처 재사용)
- Draw Call: 배치 가능
- 메모리: 단일 텍스처 크기
결과:
- 16프레임 애니메이션 렌더링 시 Draw Call 16회 → 1회로 감소
4. 유지보수성 향상
변경 전:
// 스프라이트 시트 수정 시 50줄의 코드를 전부 재작성
SpriteData[0].CuttingPos = { 0.0f, 0.0f };
SpriteData[0].CuttingSize = { 64.0f / 1024.0f, 64.0f / 1024.0f };
SpriteData[1].CuttingPos = { 64.0f / 1024.0f, 0.0f };
// ... 48줄 더
변경 후:
// Unity에서 스프라이트 시트 수정 후 .smeta 파일 덮어쓰기만 하면 끝
UEngineSprite::CreateSpriteToMeta("Character.png", ".smeta");
5. 크로스 플랫폼 에셋 공유
Unity 프로젝트 (스프라이트 편집)
↓
.smeta 내보내기
↓
DirectX 프로젝트 (자동 파싱)
↓
게임에서 사용
이점:
- Unity의 강력한 스프라이트 편집 도구 활용
📊 핵심 성과 요약
| 항목 | 개선 전 | 개선 후 | 개선율 |
|---|---|---|---|
| 스프라이트 설정 시간 | 수동 입력 | 자동 파싱 | 편의성 향상 |
| 좌표 입력 오류율 | 약 5 ~ 10% | 0% | 100% 감소 |
| Draw Call (16프레임) | 16회 | 1회 | 94% 감소 |
| 리소스 파이프라인 | Unity → 수작업 → DX | Unity → DX | 편의성 향상 |
반응형
'프로젝트 회고' 카테고리의 다른 글
| PlayerController가 입력을 처리하는게 적절한가? (0) | 2025.12.03 |
|---|---|
| [UE5 액션] [리팩토링] CPU 성능 최적화: Tick 의존성 해소, 이벤트 기반(Event-Driven Architecture)으로의 전환 (0) | 2025.12.02 |
| [UE5 팀 프로젝트] Pull-Request 시행착오와 교훈 (0) | 2025.12.02 |
| [Dedicated Server] 콜백(Callback) 체인을 활용한 데이터 무결성 확보 (0) | 2025.12.02 |
| [Dedicated Server] SeamlessTravel: 레벨 전환 시 데이터 유실 방지 (0) | 2025.12.02 |
