스프라이트 시트 노멀맵 · 프레임마다 따로 생성
노멀맵 생성기를 스프라이트 시트 전체에 돌리면 애니메이션이 아니라 그림 한 장으로 봅니다. 가까이 붙은 프레임끼리 블러와 가장자리 거리가 섞여, 같은 동작도 옆 프레임에 따라 입체감이 달라집니다. 여기서는 스프라이트 작업 공간에서 먼저 프레임을 자르고, 텍스처 작업 공간이 프레임마다 같은 px 설정으로 따로 노멀맵을 만들어 재생 중에도 입체감이 흔들리지 않습니다.
애니메이션 스프라이트 시트의 노멀맵은 프레임마다 만들어야 합니다. 각 프레임의 입체를 자기 사각형 안에서 픽셀 단위 같은 설정으로 계산하면, 색 시트와 배치가 똑같은 노멀맵 시트 한 장이 나옵니다. Nerulio에서는 스프라이트 작업 공간에서 시트를 자르고(격자나 덩어리, 그다음 Apply) 텍스처 탭을 연 뒤 _n.png와 함께, AnimationPlayer가 프레임을 넘기는 Godot 4 씬이나 같은 스프라이트 영역을 설정하고 _NormalMap을 붙이는 Unity 6 임포터를 내보냅니다.

- Godot 4 2D lights검증됨· Godot 4.7.2
- Unity 6 URP 2D검증됨· Unity 6000.5.3f1 (URP 17.5)
- PNG set + JSON일반 파일
프레임 경계에서 생기는 문제와 프레임별 생성으로 피하는 방법
노멀맵 생성기는 이웃 픽셀을 봅니다. 기울기 커널은 픽셀 둘레 1px(Pixel·Sobel·Scharr)이나 2px(Sobel 5×5)을 읽고, 베벨은 가장 가까운 투명 픽셀까지 거리를 재며, 블러는 더 멀리 퍼집니다. 시트 전체에 돌리면 셋 다 프레임 경계를 넘습니다. 두 프레임이 맞닿은 곳에서는 베벨이 옆 프레임의 픽셀을 몸통의 연장으로 보고, 커널은 경계 열에서 두 그림을 섞습니다.
Nerulio는 프레임 영역마다 따로 처리합니다. 커널은 영역 경계에서 멈추고(프레임의 마지막 픽셀을 반복), 거리 변환은 프레임 가장자리를 투명으로 보며, 블러는 경계를 넘어 읽지 않습니다. 폭·깊이·강도는 프레임마다 다시 맞추지 않는 절대 픽셀 값이라, 같은 포즈는 시트의 어디에 있든 같은 입체가 됩니다. tests/normals.test.mjs가 프레임의 맵이 이웃에 좌우되지 않고 스프라이트와 함께 움직이는지 확인합니다.
그림 바깥에는 알파가 꽉 찬 평평한 노멀 (128, 128, 255)이 들어가고, 실루엣의 노멀은 투명한 곳으로 2px 복사됩니다. 그래서 엔진이 시트를 필터링하거나 밉맵을 만들 때 스프라이트 가장자리 옆 텍셀에 빈 값이 아니라 그럴듯한 노멀이 있습니다.
조명은 프레임 픽셀 기준으로 저장되므로 미리보기와 내보낸 씬 모두 모든 프레임을 같은 상대 위치에서 비춥니다. 영역이 시트 위를 옮겨 다녀도 애니메이션이 조명 속을 흘러가지 않습니다.
- 프레임 영역
- 시트 위 프레임 하나의 사각형. 노멀맵 계산은 모두 그 안에서만 합니다.
- Clamp
- 경계 너머를 읽지 않고 마지막 픽셀을 반복하는 가장자리 방식.
- 가장자리 번짐
- 가장자리 노멀을 투명 픽셀로 복사해 실루엣에서 필터링해도 올바르게 하는 처리.
예시: 80 × 110 프레임으로 된 720 × 330 시트
Godot·Unity 검증에 쓴 CC0 모험가 시트(720 × 330은 2의 거듭제곱이 아님):
720 / 80 = 9열 330 / 110 = 3행 → 27칸
영역마다: 베벨·커널·블러가 80 × 110 안에서만 동작
노멀 시트: 720 × 330, 같은 배치, _n.png로 저장
Godot 10번 프레임 = 1행 1열 → region_rect Rect2(80, 110, 80, 110)
Unity 영역은 아래에서부터 셈: y = 330 − (110 + 110) = 110
0행 → y = 220 2행 → y = 0Godot 씬에서 프레임은 region_rect의 키 하나이며 그 앞 프레임들의 길이를 더한 시점에 놓이고, 애니메이션 길이는 전체 길이의 합입니다. CanvasTexture 하나가 모든 프레임의 노멀맵을 담습니다.
이렇게 작동합니다
- 프레임은 스프라이트 작업 공간(적용)에서 옵니다. 텍스처 작업 공간은 직접 자르지 않고, 프레임이 없는 시트는 그림 한 장으로 비춥니다.
- 거리 변환·블러·커널이 프레임 경계를 넘어 읽지 않습니다. tests/normals.test.mjs가 프레임의 맵이 이웃과 무관하고 스프라이트와 함께 움직이는지 확인합니다.
- 베벨 폭·깊이·디테일은 절대 px 값이라 프레임마다 다시 맞추지 않습니다. 128px 이하 도트 프레임에는 1.5px 테두리를 제안합니다.
- 조명 위치는 프레임 px 기준이라 모든 프레임이 똑같이 비칩니다. Enter는 조명 켠 채 재생, `는 시트 전체와 한 프레임 보기를 오갑니다.
- 높이 브러시(B)의 한 획은 시작한 프레임 안에만 남고, 실행 취소 한 번 단위입니다.
사용 방법
- 스프라이트 시트를 넣으면 스프라이트 작업 공간에서 열립니다.
- 격자와 애니메이션을 확인하고 적용을 누릅니다.
- 텍스처 탭을 열면 모든 프레임의 노멀맵이 바로 만들어집니다.
- 베벨과 디테일을 맞추고 조명 도구(L)로 빛을 놓은 뒤 Enter로 조명 속 애니메이션을 봅니다.
- 내보내기(Ctrl+E)에서 Godot 4, Unity 6 URP 2D, PNG 세트 중 하나를 고릅니다.
결과 확인
- ` 키로 시트 전체와 프레임 하나를 오가 보세요. 그 프레임의 입체가 양쪽에서 같아야 합니다.
- Lit 보기에서 Enter를 누르세요. 비슷해 보이는 두 프레임 사이에서 입체가 깜박이면 높이 보기로 비교하세요. 차이는 이웃이 아니라 그림 자체에 있습니다.
_n.png를 색 시트 옆에 열어 보면 실루엣이 픽셀 단위로 겹쳐야 합니다.- Godot에서 AnimationPlayer를 재생하면 키마다
region_rect가 한 프레임씩 넘어가고 노멀도 스프라이트와 함께 움직입니다.
시트 통째 처리와 프레임별 처리 비교
| 시트 전체를 한 번에 | 프레임별(이 작업 공간) | |
|---|---|---|
| 프레임 경계 근처의 베벨 | 가장자리 거리가 옆 프레임까지 닿을 수 있음 | 각 프레임 안에서만 측정 |
| 블러와 디테일 | 옆 프레임으로 번질 수 있음 | 프레임 경계에서 멈춤 |
| 두 프레임의 같은 그림 | 입체감이 달라질 수 있음 | 같은 입체감, 스프라이트와 함께 이동 |
| 조명 위치 | 시트 px 기준: 위치가 다른 프레임은 다르게 비침 | 프레임 px 기준: 모든 프레임이 똑같이 비침 |
| 내보내기 | 노멀맵 이미지 한 장, 엔진 설정은 직접 | AnimationPlayer가 든 Godot 씬, 유니티 스프라이트 영역+_NormalMap |
문제 해결
| 증상 | 유력한 원인 | 확인 방법 | 해결 |
|---|---|---|---|
| 프레임 한쪽 변을 따라 곧은 밝은 능선이 생김 | 그림이 프레임 가장자리에 닿아 있고, 프레임 경계는 투명으로 간주되어 거기에 테두리가 만들어짐 | 능선이 프레임 경계와 정확히 겹침 | 그림 둘레에 여유를 두고 다시 자르거나, 높이 브러시로 그 줄을 평평하게 |
| 한 포즈의 입체가 다음 포즈로 새어 들어감 | 노멀맵을 시트 전체로 만듦(다른 툴이거나 프레임을 자르지 않은 시트) | 텍스처 작업 공간에 영역이 하나뿐이고 프레임 패널이 비어 있음 | 스프라이트 작업 공간에서 프레임을 자르고(시트 자르기 참고) 다시 생성 |
| 엔진에서 스프라이트가 움직일 때 가장자리가 반짝임 | 이중선형 필터가 노멀 시트의 프레임 경계 너머를 샘플링함 | Nearest·Point 필터에서는 사라짐 | 도트는 Nearest·Point, 필터를 쓰는 그림은 프레임 사이에 간격 두기(아틀라스 여백) |
| Unity에서 시트의 엉뚱한 부분이 보임 | 내보낸 뒤 시트를 고치거나 크기를 바꿔 영역이 맞지 않음 | Sprite Editor 영역과 PNG 크기 비교 | 다시 내보내고 Apply Texture JSON 재실행 |
다른 방법
| 방법 | 이럴 때 더 낫습니다 |
|---|---|
| 아무 툴에서나 프레임을 하나씩 만들어 다시 합치기 | 프레임이 적고 이미 쓰는 툴이 있을 때. 배치를 손으로 똑같이 유지해야 합니다. |
| Laigter의 시트 나누기 | 앱에서 프레임 수나 격자 크기로 시트를 나눕니다(1.12.0 릴리스). 베벨이 프레임 경계를 어떻게 다루는지는 측정하지 않았습니다. |
색·노멀 아틀라스를 함께 패킹(TexturePacker --pack-normalmaps) | 여러 스프라이트를 아틀라스 하나로 패킹할 때. Studio 텍스처 작업 공간은 시트 한 장 단위이며 여러 스프라이트를 패킹한 아틀라스는 다루지 않습니다. |
엔진별 내보내기와 검증 방법
"검증됨"은 실제 CC0 에셋으로 내보낸 파일을 해당 엔진·도구가 직접 불러와 원본과 비교했다는 뜻입니다. 불러오지 않은 형식은 그렇게 표시합니다.
| 대상 | 파일 | 검증 |
|---|---|---|
| Godot 4 2D lights | _lit.tscn · CanvasTexture .tres · normal / specular PNG | 검증됨 Godot 4.7.2실제 CC0 사례 6개를 Godot(gl_compatibility)가 렌더링했고, 확인한 모든 프레임이 Studio 조명 미리보기와 1/255 이내로 같았습니다. 초록을 뒤집은 같은 렌더는 3.8~37단계 어긋나므로 잘못된 규약을 잡아낼 수 있습니다. |
| Unity 6 URP 2D | PNG · nerulio-texture.json · Editor/NerulioNormalMapImporter.cs | 검증됨 Unity 6000.5.3f1 (URP 17.5)12회 중 12회 통과(사례 6개 × 감마·리니어 색 공간): URP가 적용한 조명이 내보낸 노멀맵과 일치합니다(평균 오차 0.0037~0.0085). 빛의 감쇠는 URP 방식이라 밝기는 미리보기가 아니라 Unity 기준입니다. |
| PNG set + JSON | albedo · _n (OpenGL) · _n_dx (DirectX) · 16-bit height · AO · specular · manifest | 일반 파일어떤 엔진이나 도구에서도 쓰는 일반 파일이며, 엔진 실행 검증은 주장하지 않습니다. |
검증 근거
Godot 4.7.2가 실제 CC0 사례 6개(도트, HD 스프라이트, 반복 텍스처, 가져온 DirectX 맵)를 렌더링했고 확인한 모든 프레임이 Studio 미리보기와 1/255 이내였습니다. Unity 6000.5 URP 2D: 12회 중 12회 통과(사례 6개 × 감마·리니어). 규약을 아는 실제 맵으로 OpenGL/DirectX 판별: 1K 전체 맵 128개 100% 정답, 표본 1,562개 중 틀린 "높음" 판정 0건(초록이 밝으면 OpenGL이라는 단순 규칙은 46%).
한계
- 텍스처 작업 공간은 프레임을 자르지 않습니다. 프레임이 없는 시트는 그림 한 장으로 비춥니다.
- Wrap·Mirror 모드에서 영역 경계 근처의 실시간 브러시 미리보기는 칠한 뒤 정확히 다시 계산될 때까지 조금 다를 수 있습니다.
- 노멀맵을 여러 스프라이트 아틀라스와 함께 패킹하지 않습니다.
- 투명한 가장자리가 없는 그림(반복 텍스처)에서는 빨강이 뒤집힌 것과 DirectX 초록을 구별할 수 없습니다. 점검 패널이 이를 알리고 "빨강 뒤집기"를 제공합니다.
- 시트는 스프라이트 작업 공간에서 프레임을 자르기 전까지 그림 하나로 조명됩니다. 텍스처 작업 공간은 프레임을 자르지 않습니다.
- 림 라이트는 미리보기 전용이며, Unity로는 스페큘러 맵을 내보내지 않습니다(Godot는 내보냄).
- Unity는 자체 빛 감쇠를 씁니다. 내보낸 노멀맵은 Unity에서 검증했지만 밝기 곡선은 Unity 방식입니다.
버전과 출처
tests/normals.test.mjs는 프레임의 맵이 이웃에 좌우되지 않고 스프라이트와 함께 움직임을 보여 줍니다. Godot 4.7.2는 60프레임 사무라이와 720 × 330 모험가를 포함해 내보낸 AnimationPlayer 프레임을 1/255 이내로 렌더했고, 같은 6개 사례가 Unity 6000.5.3f1에서 12회 중 12회 통과했습니다. 노멀맵이 스프라이트와 UV를 공유해야 한다는 점은 Unity 매뉴얼에 있습니다.
참고한 공식 문서
자주 묻는 질문
시트 전체로 노멀맵 하나를 만들면 안 되나요?
프레임이 붙어 있거나 가까우면 시트 전체를 한 번에 처리할 때 블러와 가장자리 거리가 옆 프레임으로 넘어가, 같은 그림도 이웃에 따라 입체감이 달라지고 애니메이션이 번쩍거립니다. 프레임별로 처리하면 시트 어디에 있든 같은 결과가 나옵니다.
Godot 내보내기에서 애니메이션도 재생되나요?
네. <이름>_lit.tscn에 CanvasTexture를 쓴 Sprite2D, 주변광용 CanvasModulate, 조명마다 PointLight2D, 프레임을 넘기는 AnimationPlayer가 들어 있습니다. Godot 4.7.2 렌더링은 그 AnimationPlayer를 거친 경우를 포함해 확인한 모든 프레임에서 Studio 기준과 1/255 이내로 같았습니다.
유니티에서는요?
Unity 6 URP 2D 번들은 JSON으로 스프라이트 영역을 잡고 노멀맵을 _NormalMap 보조 텍스처(선형, 무압축, 리샘플링 없음)로 붙입니다. Unity 6000.5.3f1에서 12번 모두 통과했고, 밝기 곡선은 URP 자체의 것입니다.
무료인가요? 계정이 필요한가요?
이 페이지의 기능은 모두 무료이며 계정 없이 쓸 수 있습니다. 워터마크나 막혀 있는 내보내기가 없습니다.
스프라이트가 업로드되나요?
아니요. Studio는 이 브라우저 탭 안에서 디코딩·패킹·내보내기를 합니다. 프로젝트는 브라우저(IndexedDB)에 자동 저장되며 .nerulio 파일로 저장할 수 있습니다.
Aseprite나 엔진 플러그인을 설치해야 하나요?
아니요. .aseprite 파일은 브라우저에서 읽고 씁니다. 엔진별 번들에는 그 엔진에 필요한 것이 들어 있습니다(Godot는 가져오기 설정과 씬, Unity와 Godot 타일셋은 작은 편집기 스크립트).