고도 엔진 2D 노멀맵 · 조명 씬까지 한 번에 내보내기
Godot 4에서 2D 노멀맵은 넣기만 해서는 아무 변화가 없습니다. CanvasTexture 안에 들어가야 하고, 높이가 있는 Light2D가 스프라이트를 비춰야 합니다. 텍스처 작업 공간은 노멀맵을 만들고 Godot 캔버스 렌더러와 같은 식으로 비춰 본 뒤, 스프라이트·조명·주변광·프레임이 다 들어간 씬을 내보냅니다. Godot 4.7.2 렌더와 미리보기의 차이는 1/255 이내였습니다.
Godot 4에서 2D 노멀맵은 CanvasTexture 안에서만 작동합니다. 스프라이트의 Diffuse Texture 옆 Normal Map Texture에 노멀맵을 넣고, Height가 0보다 크며 Item Cull Mask가 스프라이트의 Light Mask와 겹치는 PointLight2D나 DirectionalLight2D로 비춰야 합니다. Nerulio 텍스처 작업 공간은 노멀맵을 만들고, 그 씬(Sprite2D, CanvasTexture, CanvasModulate, 조명마다 PointLight2D, 프레임용 AnimationPlayer)을 완성된 상태로 내보냅니다. Godot 4.7.2 렌더와 미리보기 차이는 1/255 이내였습니다.

- Godot 4 2D lights검증됨· Godot 4.7.2
- Unity 6 URP 2D검증됨· Unity 6000.5.3f1 (URP 17.5)
- PNG set + JSON일반 파일
Godot 4가 노멀맵 스프라이트를 비추는 방식
Godot는 스프라이트 색 위에 2D 조명을 픽셀 단위로 그립니다. 일반 텍스처에는 노멀이 없어 조명이 밝게만 할 뿐이고, 디퓨즈 텍스처·노멀맵·스페큘러 맵을 묶는 CanvasTexture에 넣어야 합니다. 문서대로 노멀맵은 Light2D가 그 CanvasTexture에 영향을 줄 때만 눈에 보입니다. Godot가 기대하는 것은 X+, Y+, Z+ 즉 OpenGL 방식입니다.
조명은 픽셀마다 색 × 에너지 × 감쇠 × N·L을 더하며, L은 픽셀에서 Height만큼 들어 올린 조명으로 향하는 방향입니다. PointLight2D의 Height는 픽셀 단위로, 문서의 예는 높이 100이 100px 떨어진 물체를 45°로 비춘다는 것입니다. 기본값은 0이라 조명이 스프라이트 평면 위에 놓입니다. DirectionalLight2D의 Height는 0(평면과 평행)부터 1(수직)까지이고 방향은 노드의 회전으로 정합니다.
조명의 크기와 감쇠는 텍스처에서 옵니다. PointLight2D는 보통 회색인 텍스처를 Texture Scale만큼 키워 그립니다. CanvasModulate는 캔버스 전체에 주변광 색을 곱해 조명이 닿지 않는 곳을 어둡게 만듭니다. 조명이 어느 노드에 닿는지는 Range 설정이 정하는데, 조명의 Item Cull Mask와 각 CanvasItem의 Light Mask(기본 1번 레이어), 그리고 레이어·z 범위입니다.
Nerulio의 미리보기는 이 모델을 CPU와 WebGL2로 계산한 것입니다(lighting.js). Godot처럼 빨강·초록에서 N을 읽고 파랑은 그 둘로 다시 계산하며, 내보내는 조명마다 Studio의 감쇠 곡선을 256px 텍스처로 넣기 때문에 방향뿐 아니라 밝기까지 미리보기와 같습니다.
- CanvasTexture
- 디퓨즈·노멀·스페큘러 맵과 스페큘러 색·광택을 담는 2D 텍스처 리소스.
- Height
- 조명이 캔버스 위로 얼마나 떠 있는지. 노멀맵이 있는 면에서만 차이가 보입니다.
- Item Cull Mask / Light Mask
- 조명과 스프라이트의 레이어 비트. 하나라도 겹쳐야 조명이 닿습니다.
- CanvasModulate
- 캔버스마다 하나 두는 노드로, 모든 것에 주변광 색을 곱합니다.
예시: Height와 Texture Scale이 한 픽셀에 주는 영향
평평한 픽셀(N = 0, 0, 1)과 그 왼쪽 64px에 있는 PointLight2D:
Height 64px → 앙각 atan(64/64) = 45° 평면 N·L = sin 45° = 0.707
Height 0(기본) → 앙각 0° 평면 N·L = 0: 조명을 향한 경사만 밝음
Height 256px → 앙각 atan(256/64) = 76° 평면 N·L = 0.970: 입체감이 거의 사라짐
Nerulio 감쇠 PNG 폭 = 256px 이므로 texture_scale = 2 × 반경 / 256
반경 128px → texture_scale 1.0 반경 256px → texture_scale 2.0
DirectionalLight2D: Height 0 = 평면과 평행, 1 = 바로 위에서 수직낮은 조명은 입체를 과장하고 높은 조명은 평평하게 만드니, 노멀맵 강도를 바꾸기 전에 Height부터 조절하세요.
이렇게 작동합니다
- <name>_lit.tscn: CanvasTexture(디퓨즈 + 노멀)를 텍스처로 쓰는 Sprite2D, 주변광 색을 맡는 CanvasModulate, 조명마다 PointLight2D 하나, 그리고 자른 시트라면 region_rect를 프레임마다 옮기는 AnimationPlayer.
- PointLight2D마다 그 조명의 감쇠(smooth·linear·quadratic·none)가 텍스처로 들어가고, 색·세기·높이(픽셀 단위 z)도 그대로 옮겨집니다.
- 직접 만든 Sprite2D에 쓸 <name>_canvas_texture.tres. 스페큘러를 0보다 크게 두면 스페큘러 맵과 specular_shininess가 추가됩니다.
- 조명 보기는 Godot 캔버스 조명 식(기본색 × 주변광 + 감쇠 × 조명 × 세기 × (기본색 × N·L + 스페큘러))을 그대로 쓰고, WebGL2 화면은 CPU 기준 계산과 1/255 이내로 같습니다.
- 점 조명은 최대 8개. 림 라이트는 커스텀 셰이더 없이는 Godot가 그리지 않으므로 미리보기 전용입니다.
사용 방법
- 스프라이트나 시트를 넣습니다. 시트는 먼저 스프라이트 작업 공간에서 잘라야 프레임이 움직입니다.
- 노멀 맵 패널에서 베벨과 디테일을 확인하고, 높이 브러시(B)로 튀어나오거나 들어갈 곳을 고칩니다.
- L을 눌러 조명을 놓고 높이·세기·감쇠를 정한 뒤, 조명 보기에서 Enter로 재생해 봅니다.
- 내보내기(Ctrl+E)에서 Godot 4를 체크합니다.
- godot 폴더를 res:// 아래 아무 곳에 복사하고 <name>_lit.tscn을 엽니다.
받게 되는 파일
torch.png라는 스프라이트라면 godot/ 폴더에 다음이 들어 있습니다.
| 파일 | 용도 |
|---|---|
torch_lit.tscn | 주변광 CanvasModulate, Sprite2D(region 사용, 도트면 Nearest), 조명마다 PointLight2D, 프레임이 있으면 AnimationPlayer가 든 Node2D. |
torch_canvas_texture.tres | 내 Sprite2D에 쓸 CanvasTexture 단독 파일(디퓨즈, 노멀, 필요하면 스페큘러와 광택). |
torch.png, torch_n.png | 원본과 바이트까지 같은 스프라이트·시트, 그리고 OpenGL 노멀맵. |
torch_light_smooth.png | 쓰인 감쇠 종류(smooth·linear·quadratic·constant)마다 하나씩인 감쇠 텍스처. |
torch_s.png | 스페큘러가 0보다 클 때만 들어가는 스페큘러 맵. |
README.md | 이 단계와 검증 상태. |
Godot 4에서 설정하기
godot폴더를res://아래 아무 곳에 복사하고(파일끼리 상대 경로로 참조)torch_lit.tscn을 열어 결과를 봅니다. 아래는 내 노드에 직접 설정할 때입니다.- Sprite2D를 선택하고 Texture 속성에서 새 CanvasTexture를 만든 뒤 Diffuse Texture에 스프라이트 PNG, Normal Map Texture에
torch_n.png를 넣거나torch_canvas_texture.tres를 불러옵니다. - PointLight2D를 추가하고 Texture(번들의 감쇠 PNG나 부드러운 원형 그러데이션), 0보다 큰 Height(픽셀), Energy를 정한 뒤 스프라이트 위로 옮깁니다.
- 마스크를 확인합니다. 조명의 Range › Item Cull Mask와 스프라이트의 Light Mask(기본 1번 레이어)가 한 레이어 이상 겹쳐야 합니다.
- 어두운 색의 CanvasModulate를 하나 두어 조명이 없는 곳을 어둡게 합니다. 없으면 스프라이트가 처음부터 최대 밝기이고 조명은 더하기만 합니다.
- 도트 그림이면 Sprite2D의 Texture › Filter를 Nearest로 둡니다(번들은 설정됨). 다른 곳에서 받은 DirectX 방식 맵이라면 Import 독에서 Process › Normal Map Invert Y를 켜고 다시 가져옵니다.
결과 확인
- 조명을 스프라이트 위로 옮기세요. 윗가장자리가 밝아져야 하고, 아랫가장자리가 밝아지면 DirectX 방식 맵입니다.
- 조명의 Height를 0부터 올려 보세요. 조명이 높아질수록 입체가 부드러워져야 하며, 전혀 변화가 없으면 노멀맵이 읽히지 않는 것입니다.
- Studio의 Lit 보기와 비교하세요. 검증에서 Godot 4.7.2는 확인한 모든 프레임에서 1/255 이내로 같았습니다.
Godot 번들에 들어 있는 것
| 파일·노드 | 내용 | 내 게임에서 할 일 |
|---|---|---|
| <name>_lit.tscn의 Sprite2D | 텍스처 = CanvasTexture, region 사용, 도트면 Nearest 필터 | 씬을 인스턴스하거나 텍스처만 내 스프라이트로 복사 |
| <name>_canvas_texture.tres | diffuse_texture, normal_texture, 필요하면 specular_texture와 specular_shininess | 같은 시트를 그리는 Sprite2D에 재사용 |
| PointLight2D(조명마다 하나) | 프레임 픽셀 기준 위치, color, energy, height = z, 감쇠 PNG 텍스처, 반경에서 계산한 texture_scale | 하나는 횃불로 남기고 나머지는 지우거나, 값을 레벨 조명에 옮기기 |
| CanvasModulate "Ambient" | 주변광 색 | 레벨에 하나만 두기(캔버스 전체에 색이 곱해짐) |
| AnimationPlayer | 태그마다 애니메이션 하나, 프레임 시작마다 region_rect 키, 태그대로 반복 | 스크립트에서 태그 이름으로 play() 호출 |
| PNG 파일 | <name>.png, <name>_n.png(OpenGL), 스페큘러 맵, 쓰인 감쇠마다 PNG 하나 | 씬 옆에 두기(상대 경로) |
림 라이트는 내보내지 않습니다(미리보기 전용). 폴더의 README에 검증 상태가 다시 적혀 있습니다.
문제 해결
| 증상 | 유력한 원인 | 확인 방법 | 해결 |
|---|---|---|---|
| 노멀맵을 넣어도 아무 변화가 없음 | 스프라이트가 노멀맵이 든 CanvasTexture가 아닌 일반 텍스처를 씀 | 인스펙터에서 Sprite2D의 Texture가 CanvasTexture인지 | 텍스처를 CanvasTexture로 감싸거나 torch_canvas_texture.tres 불러오기 |
| 조명 쪽 테두리만 빛나고 가운데는 어두움 | PointLight2D의 Height가 기본값 0이라 조명이 스프라이트 평면에 놓임 | 인스펙터에서 조명의 Height | Height를 올리기. 스프라이트까지의 거리만큼이면 45° |
| 스프라이트가 전혀 밝아지지 않음 | 조명의 Item Cull Mask와 스프라이트의 Light Mask가 겹치지 않거나, 스프라이트가 조명 텍스처 범위 밖에 있음 | 조명을 껐다 켜 보고, 두 마스크와 Texture Scale 확인 | 같은 레이어에 두고 Texture Scale 키우기 |
| 조명이 위에 있는데 아래에서 비친 듯함 | DirectX 방식(Y−) 노멀맵 | 바로 위 조명에서 윗가장자리가 어두움 | OpenGL인 torch_n.png를 쓰거나 Process › Normal Map Invert Y를 켜고 다시 가져오기. OpenGL·DirectX 판별 참고 |
| 화면 전체가 허옇게 뜸 | CanvasModulate가 없어 기본색이 이미 최대 밝기 | 씬 트리에 CanvasModulate가 없음 | 번들의 Ambient 노드처럼 어두운 색으로 하나 추가 |
| 도트 입체가 번져 보임 | 노드나 CanvasTexture의 선형 필터 | 확대하면 픽셀 경계가 부드러움 | Nearest 필터. Godot 도트 흐림 해결 참고 |
다른 방법
| 방법 | 이럴 때 더 낫습니다 |
|---|---|
| CanvasTexture와 조명을 직접 구성 | 이미 노멀맵이 있을 때(3D 굽기, Laigter, 손으로 그림). 위 단계를 내 _n 파일로 따라 하면 됩니다. |
| 직접 만든 canvas_item 셰이더 | 림 라이트처럼 Godot의 Light2D가 그리지 않는 조명이 필요할 때. 그래서 Studio의 림 라이트는 미리보기에만 있습니다. |
| SpriteFrames 내보내기의 AnimatedSprite2D | 애니메이션을 SpriteFrames로 다루고 싶을 때. 그 프레임에 노멀맵을 붙이는 것은 검증 대상이 아니었습니다. |
엔진별 내보내기와 검증 방법
"검증됨"은 실제 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%).
한계
- Godot 4.7.2의 Compatibility 렌더러(gl_compatibility)로만 확인했습니다. Forward+와 Mobile은 돌려 보지 않았습니다.
- 그림자(LightOccluder2D)는 내보내지 않습니다.
- 내보낸 씬은 AnimatedSprite2D가 아니라 AnimationPlayer로 Sprite2D를 움직입니다.
- 투명한 가장자리가 없는 그림(반복 텍스처)에서는 빨강이 뒤집힌 것과 DirectX 초록을 구별할 수 없습니다. 점검 패널이 이를 알리고 "빨강 뒤집기"를 제공합니다.
- 시트는 스프라이트 작업 공간에서 프레임을 자르기 전까지 그림 하나로 조명됩니다. 텍스처 작업 공간은 프레임을 자르지 않습니다.
- 림 라이트는 미리보기 전용이며, Unity로는 스페큘러 맵을 내보내지 않습니다(Godot는 내보냄).
- Unity는 자체 빛 감쇠를 씁니다. 내보낸 노멀맵은 Unity에서 검증했지만 밝기 곡선은 Unity 방식입니다.
버전과 출처
Godot 4.7.2(gl_compatibility), 실제 CC0 사례 6건: 확인한 모든 프레임이 Studio 기준과 1/255 이내(평균 0.14~0.22단계)였고 내보낸 AnimationPlayer를 거쳐도 같았습니다. 초록을 뒤집은 렌더는 3.8~37단계 어긋나, 검사가 잘못된 규약을 잡아낸다는 것도 확인했습니다. 속성 이름과 기본값은 Godot 4.7 클래스 레퍼런스를 따릅니다.
참고한 공식 문서
자주 묻는 질문
고도 2D에서 노멀맵을 넣었는데 아무 변화가 없어요.
대개 셋 중 하나입니다. 노멀맵이 CanvasTexture의 normal_texture에 들어가 있지 않거나, 스프라이트까지 닿는 Light2D가 없거나, 조명 높이가 0이라 옆에서 스치듯 비추는 경우입니다. 내보낸 씬은 셋 다 설정되어 있으니 내 노드와 비교해 보세요.
AnimatedSprite2D를 쓰나요?
아니요. Sprite2D를 쓰고 AnimationPlayer가 시트 위에서 region_rect를 옮깁니다. CanvasTexture 하나가 모든 프레임의 노멀맵을 담습니다. 검증에서 Godot 4.7.2가 렌더한 것도 이 구성이며, AnimatedSprite2D 구성은 확인 대상이 아니었습니다.
어떤 렌더러와 에셋으로 확인했나요?
Godot 4.7.2 Compatibility 렌더러(gl_compatibility)에서 실제 CC0 에셋 6건입니다. 도트 횃불(스페큘러 유무), 60프레임 사무라이, 80×110 모험가, 반복 벽돌 텍스처, 가져온 DirectX 노멀맵. 확인한 모든 프레임이 1/255 이내였고, 초록을 뒤집은 렌더는 3.8~37단계 어긋났습니다.
Godot에서 밝기도 미리보기와 같나요?
네. 조명마다 Studio의 감쇠 곡선이 텍스처로 들어가서 곡선이 같고, 검증도 최종 조명 픽셀을 비교했습니다. Unity는 URP가 자체 감쇠를 쓰므로 다릅니다.
무료인가요? 계정이 필요한가요?
이 페이지의 기능은 모두 무료이며 계정 없이 쓸 수 있습니다. 워터마크나 막혀 있는 내보내기가 없습니다.
스프라이트가 업로드되나요?
아니요. Studio는 이 브라우저 탭 안에서 디코딩·패킹·내보내기를 합니다. 프로젝트는 브라우저(IndexedDB)에 자동 저장되며 .nerulio 파일로 저장할 수 있습니다.
Aseprite나 엔진 플러그인을 설치해야 하나요?
아니요. .aseprite 파일은 브라우저에서 읽고 씁니다. 엔진별 번들에는 그 엔진에 필요한 것이 들어 있습니다(Godot는 가져오기 설정과 씬, Unity와 Godot 타일셋은 작은 편집기 스크립트).