프로젝트 회고 / / 2025. 12. 2. 23:20

[DirectX 11] Unity 리소스 메타데이터 파싱을 통한 스프라이트 시트 관리

반응형

🎮 구현 목표

Unity에서 제작된 스프라이트 시트의 메타데이터(.smeta) 파일을 파싱하여, DirectX 11 엔진에서 개별 스프라이트의 UV 좌표와 크기 정보를 자동으로 추출하고 관리하는 시스템을 구축합니다.

 

 

🚨 문제 상황

  1. 반복적인 수작업의 비효율성
  • 리소스를 포토샵에서 하나 하나 크기에 맞춰 잘라야하는 수작업
  • 리소스를 분석하여 프레임 순서에 맞춰 파일 이름을 정렬
  • 스프라이트 시트에서 각 프레임의 픽셀 좌표와 크기를 일일이 계산
  • 프로그램에서 렌더링 후 피봇 불일치 시, 다시 포토샵으로 작업 후 업로드하는 불편함
  1. 유지보수의 어려움
  • 스프라이트 인덱스를 직접 계산하고 체크 (위험)

사전에 리소스를 준비하고 편집하는 시간이 상당히 오래걸리고, 리소스를 추가할 때마다 수 시간이 소요되는 작업 부하가 발생했습니다.
리스소와 관련된 작업의 부담을 해소하기 위해 파일시스템과 데이터 파싱을 이용하기로 했습니다.

 

 

 💭 해결 방안

아키텍처

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;

 

파싱 로직:

  1. rect: 키워드로 스프라이트 데이터 시작 지점 탐색
  2. outline: 키워드로 종료 지점 확인
  3. 두 키워드 사이의 텍스트 블록을 추출하여 개별 스프라이트 데이터로 분리

 

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;

 

이 공식은 다음과 같이 동작합니다:

  1. TexSize.Y: 텍스처 전체 높이
  2. CuttingPos.Y: Unity 좌표계의 Y 위치
  3. CuttingSize.Y: 스프라이트 높이
  4. 결과: 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 편의성 향상
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유