Unity / C#
Advanced
Unity 2022.3+ / Unity 6
3D 애니메이션 모델의 8방향 2D 픽셀 스프라이트 & 노멀맵 자동 베이킹(Baking) 툴
Dead Cells 및 Factorio 제작 방식: 3D 모델과 본 애니메이션을 지정된 각도·프레임별로 캡처해 2D 픽셀 스프라이트 시트와 노멀맵 PNG를 자동 굽는 에디터 툴.
#Unity #Editor #SpriteSheet #Baker #3D-to-2D #Tool
핵심 개념복잡한 3D 갑옷 캐릭터의 8방향 걷기·공격 모션을 2D 도트로 직접 찍는 것은 수개월이 걸립니다. *Dead Cells*와 *Factorio* 개발팀은 3D 모델에 모션 캡처나 3D 애니메이션을 적용한 뒤, 에디터 스크립트로 8개 각도에서 저해상도로 자동 캡처하여 2D 스프라이트 시트로 구워내는 파이프라인을 구축해 개발 기간을 90% 이상 단축했습니다.
1. 3D-to-2D 스프라이트 시트 자동 베이커 에디터 스크립트
#if UNITY_EDITOR
using System.IO;
using UnityEditor;
using UnityEngine;
public class SpriteSheetBakerWindow : EditorWindow
{
[SerializeField] private GameObject targetModel;
[SerializeField] private AnimationClip clipToBake;
[SerializeField] private int frameRate = 12; // 레트로 도트 프레임 (12fps)
[SerializeField] private int resolution = 64; // 프레임당 해상도 (64x64)
[SerializeField] private int directionCount = 8; // 8방향
[MenuItem("Tools/LYSC/3D to 2D Sprite Baker")]
public static void ShowWindow()
{
GetWindow<SpriteSheetBakerWindow>("Sprite Baker");
}
private void OnGUI()
{
GUILayout.Label("3D 모델 -> 2D 픽셀 스프라이트 시트 베이커", EditorStyles.boldLabel);
targetModel = (GameObject)EditorGUILayout.ObjectField("3D 모델 프리팹", targetModel, typeof(GameObject), true);
clipToBake = (AnimationClip)EditorGUILayout.ObjectField("애니메이션 클립", clipToBake, typeof(AnimationClip), false);
frameRate = EditorGUILayout.IntSlider("베이킹 FPS", frameRate, 6, 24);
resolution = EditorGUILayout.IntField("스프라이트 해상도 (px)", resolution);
directionCount = EditorGUILayout.IntPopup("방향 수", directionCount, new[] { "1방향", "4방향", "8방향" }, new[] { 1, 4, 8 });
if (GUILayout.Button("스프라이트 시트 베이킹 시작", GUILayout.Height(35)))
{
BakeSpriteSheet();
}
}
private void BakeSpriteSheet()
{
if (targetModel == null || clipToBake == null)
{
EditorUtility.DisplayDialog("오류", "모델과 클립을 모두 등록해야 합니다.", "확인");
return;
}
int totalFrames = Mathf.CeilToInt(clipToBake.length * frameRate);
int sheetWidth = totalFrames * resolution;
int sheetHeight = directionCount * resolution;
Texture2D sheet = new Texture2D(sheetWidth, sheetHeight, TextureFormat.RGBA32, false);
// 캡처용 임시 카메라 및 렌더텍스처 구성
GameObject camObj = new GameObject("TempBakeCamera");
Camera bakeCam = camObj.AddComponent<Camera>();
bakeCam.orthographic = true;
bakeCam.orthographicSize = 1.0f;
bakeCam.clearFlags = CameraClearFlags.SolidColor;
bakeCam.backgroundColor = new Color(0, 0, 0, 0);
RenderTexture rt = new RenderTexture(resolution, resolution, 24, RenderTextureFormat.ARGB32)
{
filterMode = FilterMode.Point
};
bakeCam.targetTexture = rt;
// 각도별, 프레임별 샘플링
for (int dir = 0; dir < directionCount; dir++)
{
float angle = dir * (360f / directionCount);
targetModel.transform.rotation = Quaternion.Euler(0f, angle, 0f);
for (int f = 0; f < totalFrames; f++)
{
float time = (float)f / frameRate;
clipToBake.SampleAnimation(targetModel, time);
// 카메라 렌더링
bakeCam.Render();
// 픽셀 복사
RenderTexture.active = rt;
Texture2D tempFrame = new Texture2D(resolution, resolution, TextureFormat.RGBA32, false);
tempFrame.ReadPixels(new Rect(0, 0, resolution, resolution), 0, 0);
tempFrame.Apply();
// 시트에 복사 (행: 방향, 열: 프레임)
int startX = f * resolution;
int startY = (directionCount - 1 - dir) * resolution;
sheet.SetPixels(startX, startY, resolution, resolution, tempFrame.GetPixels());
}
}
sheet.Apply();
// PNG 파일 저장
byte[] bytes = sheet.EncodeToPNG();
string path = Path.Combine(Application.dataPath, $"{targetModel.name}_{clipToBake.name}_Sheet.png");
File.WriteAllBytes(path, bytes);
AssetDatabase.Refresh();
// 정리
bakeCam.targetTexture = null;
RenderTexture.active = null;
DestroyImmediate(camObj);
rt.Release();
EditorUtility.DisplayDialog("완료", $"스프라이트 시트가 생성되었습니다:
{path}", "확인");
}
}
#endif
2. 베이킹 파이프라인의 이점
- 12fps 도트 애니메이션 느낌 완벽 재현: 3D 모션의 너무 매끄러운 60fps 움직임을 12fps로 샘플링하여 레트로 도트 특유의 경쾌한 타이밍을 자동 생성.
- 드로우콜 1개로 압축: 3D 스킨드 메시(Skinned Mesh) 렌더러의 무거운 본 연산과 드로우콜을 평면 2D 스프라이트 렌더러로 치환하여 모바일에서 수백 마리의 적이 동시에 등장 가능.