2026-09-26

AIAssetStudio 개발기

게임을 만들다 보면 캐릭터 스프라이트와 아이콘, BGM이 끊임없이 필요하다. 그동안은 외부 이미지 생성 도구로 원본을 뽑은 뒤 게임별 스크립트로 배경을 지우고 팔레트와 픽셀 규격을 맞춰왔는데, 프로젝트가 바뀔 때마다 후처리 스크립트를 새로 짜는 게 일이었다. 그렇다고 ComfyUI를 매번 쓰자니 노드 값을 일일이 손봐야 해서 번거로웠다. 결국 로컬 모델을 돌려 프로젝트 화풍과 규격에 딱 맞는 이미지와 BGM을 바로 뽑아주는 파이프라인, AIAssetStudio를 직접 만들기로 했다. 9월 24일 기획과 기술 검증을 거쳐 25일까지 이틀간 집중해서 구현했다.

보컬 없는 BGM부터 확인하기

가장 불확실했던 건 음악이었다. 보컬 곡 모델인 YuE2는 연주곡 모드가 따로 없어 가짜 보컬(허밍 등)이 자꾸 섞였고, 과연 로컬에서 루프용 BGM을 안정적으로 뽑을 수 있을지 의문이었다. 기능을 설계하기 전 연주곡 모델인 ACE-Step 1.5로 마을과 전투 곡 4곡을 먼저 뽑아봤다. 보컬 분리 모델(HDemucs)로 측정해보니 보컬 성분이 00.9% 수준으로, 같은 프롬프트로 만든 YuE2(2736%)와 달리 사실상 순수 연주곡이었다. 연주곡은 ACE-Step에 맡기고, YuE2는 보컬 곡 옵션으로만 남기기로 정리했다.

루프 곡은 끝부분의 무음을 자르고 시작부의 조용한 마디를 건너뛴 뒤, 온전한 4/4박자 마디 단위로 끊어 30ms 크로스페이드로 이었다. ACE-Step에 96 BPM을 요청해도 실제로는 98.01 BPM 곡이 나오는 경우가 있어서, 고정 요청값 대신 음원에서 직접 검출한 템포를 기준으로 마디를 자르게 했다.

Claude Code에 시키고 갤러리에서 고르기

생성 작업은 코딩 중인 Claude Code 세션에서 MCP 도구로 호출하고, 웹 화면은 결과물을 확인하고 고르는 갤러리 역할만 맡겼다. “이 화풍으로 그림자 몬스터 스프라이트 4장 뽑아줘”, “2번 골라서 enemy_shadow로 내보내줘”처럼 대화로 지시하는 흐름이다. 특히 BGM은 Claude가 직접 들을 수 없으니 사람이 갤러리에서 루프 이음새를 들어보고 선택해야 한다.

백엔드가 ComfyUI 호출, 작업 대기열, 후처리, 저장을 전담하고 MCP 서버와 갤러리는 백엔드 HTTP API만 호출한다. 16GB VRAM에 이미지 모델과 음악 모델이 동시에 올라가지 않기 때문에 GPU 작업은 순차 처리하며, 작업 종류가 바뀔 때마다 VRAM을 비우고 모델을 교체하도록 구성했다.

사소한 이슈로 Claude Code 세션을 닫으면 백엔드와 ComfyUI까지 함께 꺼지는 문제가 있었다. 세션 내부 프로세스가 Windows Job Object에 묶여 강제 종료되던 현상이었는데, WMI를 통해 프로세스를 띄워 세션 종속성을 분리해 해결했다.

투명 스프라이트 멈춤 풀기

이미지는 Qwen-Image-2.1에 6스텝 가속 LoRA(Viggle Turbo)를 붙여 투명 배경 PNG로 뽑는다. 첫 세팅 후 포션 아이콘을 뽑는데 VAE 디코드 단계에서 6분 넘게 멈추다 타임아웃이 났다. 정작 샘플링 자체는 13초밖에 안 걸렸다. 로그를 확인해보니 텍스트 인코더가 VRAM을 잡고 있는 상태에서 디코드가 돌면 VRAM이 넘쳐 Windows가 시스템 RAM으로 스왑하면서 극단적으로 느려지는 현상이었다. 텍스트 인코딩을 먼저 끝내고 모델을 메모리에서 내린 뒤 디코드를 돌리자 장당 0.7~1.0초 수준으로 단축됐다.

아이콘 주변에 원치 않는 원형 프레임이 자꾸 생기는 문제도 있었다. 프롬프트에 no frame, no border, no ring을 넣어도 무시되길래 후처리 로직을 추가했다. 대상의 중심을 둘러싸면서 바운딩 박스 면적의 20%도 채우지 못하는 외곽 조각을 링으로 판별해 자동으로 제거했다.

참고 이미지로 화풍 맞추기

게임 안 화풍을 맞추려고 참고 이미지를 받도록 설계했다. 처음에는 "image 1은 화풍 참고용이고 새 대상을 그려라"라고 지시했더니, 방패 든 해골을 참고로 슬라임을 요청해도 해골을 그대로 다시 그렸다. 참고 이미지 속 오브젝트나 자세를 그리지 말라고 강조해도 6장 모두 해골이 나왔다. 모델이 참고 이미지를 ‘스타일 견본’이 아니라 ‘편집할 원본’으로 인식하는 듯했다. 프롬프트를 "image 1의 대상을 슬라임으로 변경하되 화풍은 유지하라"는 수정 지시 형태로 바꾸자 비로소 슬라임이 참고 화풍에 맞춰 나왔다.

주인공 그림(왼쪽)을 참고로 그린 슬라임과 망령. 아랫줄은 32칸으로 줄여 본 모습

다만 결과물이 참고 이미지의 구도와 실루엣을 강하게 따라가는 경향이 있었다. 개발 중인 픽셀아트 로그라이크의 주인공을 참고로 적 4장을 뽑았더니 한 장은 주인공과 거의 흡사하게 나왔다. 다시 뽑은 적은 색감까지 따라가 ‘어두운 주인공’처럼 보이기도 했는데, 프롬프트에 적의 고유 색상을 명시해주니 색은 의도대로 분리됐다.

픽셀 격자 되살리기

픽셀아트 프로젝트는 캔버스 규격과 색상 팔레트 수를 지정하면 결과물을 정돈해 준다. 처음에는 생성된 이미지를 캔버스 크기에 맞춰 단순히 리사이징했다. 그랬더니 모델이 작게 그린 슬라임을 32칸 캔버스에 늘리는 과정에서 1픽셀이 1칸이 되기도 하고 2칸이 되기도 해 눈 모양이 뭉개졌다. 이를 해결하기 위해 모델이 그린 픽셀 경계의 주기를 찾아 본래 그리드를 복원하고, 정수 배율로만 확대하도록 바꿨다. 격자가 불규칙하면 보간을 적용하고, 1배율 상태라 캔버스를 다 채우지 못하더라도 도트 왜곡을 막기 위해 그대로 둔다.

완성된 스프라이트를 기존 게임 리소스와 나란히 둬보니 발 위치가 2px 정도 떠 있었다. 참고 이미지가 있으면 기준선(바닥 라인)을 감지해 같은 높이에 세우도록 정렬 로직을 보정했다.

애니메이션만 클라우드로 만들기

스프라이트 1장으로 대기 4프레임, 이동 6프레임 애니메이션을 만드는 작업은 로컬 대신 sprite-gen 도구와 OpenAI Codex CLI의 이미지 생성을 연동했다. 이 스튜디오에서 유일하게 외부 API를 쓰는 부분이라 프로젝트 설정에서 명시적으로 켰을 때만 동작하도록 기본값은 꺼두었다.

테스트해보니 생성 모델이 베이스 스프라이트보다 더 큰 해상도로 프레임을 그리는 문제가 있었다. 원본은 높이 26px인데 대기 프레임은 33~40px로 나왔다. sprite-gen이 줄별 최대 프레임 기준으로 스케일을 맞추다 보니 적이 걷기 시작하면 갑자기 3px 커지는 현상이 발생했다. 결국 원본을 2배 해상도로 다시 추출해 정밀한 프레임을 얻은 뒤, 프레임별 높이를 원본 스프라이트(26px)에 맞춰 일괄 조정했다.

대기 4프레임에 이어 이동 6프레임

확인한 것과 남은 것

자동화 테스트 231개, 실 GPU 테스트 3개, 린트와 갤러리 빌드까지 통과했고, 각 단계마다 MCP 클라이언트로 실제 Claude Code의 호출 흐름을 검증했다. 픽셀아트 게임에 쓸 적 스프라이트, 아이콘, 전투 BGM 2곡, 애니메이션 시트까지 직접 뽑아봤다. 기술적인 파이프라인은 완성됐지만, 에셋의 품질이나 BGM 루프의 자연스러움은 결국 사람이 귀와 눈으로 직접 검수해야 한다.

탱글 젤리 프로젝트에 젤리 스프라이트를 적용해봤다가 다시 걷어냈다. 눈동자나 특수 젤리 심볼이 투명하게 날아가거나 그리드를 벗어나는 문제가 있었다. 또한 Qwen-Image-2.1과 Viggle Turbo가 비상업적 연구·평가용 라이선스라, 상용 게임 에셋으로 쓰려면 별도 라이선스를 취득하거나 모델을 교체해야 하는 과제가 있다. (ACE-Step은 MIT 라이선스라 문제없다.)

참고 이미지의 구도를 따라가는 문제는 아직 남아 있고, 캐릭터 4방향 시트와 타일셋·배경 생성은 계획 문서에 다음 후보로 올려 두었다. 전체 코드는 GitHub에 MIT 라이선스로 올려두었다.

같은 작업대에서 나온 이웃 도구입니다.