텍스처 가장자리 번짐 · 투명 스프라이트의 검은 테두리 해결
이중선형 필터와 밉맵은 투명 텍셀의 색을 가장자리에 섞습니다. 가장자리 번짐은 그 텍셀을 스프라이트 자신의 색으로 채우며, 알파는 절대 쓰지 않습니다.
투명한 스프라이트·잎·데칼 주위의 검은(또는 흰) 테두리는 텍스처의 완전히 투명한 픽셀에 검정이나 흰색 RGB가 남아 있고, 이중선형 필터나 작은 밉맵 레벨이 그 숨은 색을 보이는 가장자리에 섞기 때문에 생깁니다. 해결책은 그 픽셀을 가장자리 자신의 색으로 채우는 것(가장자리 번짐, 딜레이션, 알파 패딩)이거나, 엔진이 가져올 때 하게 하는 것(Godot의 Fix Alpha Border, Unity의 Alpha is Transparency)입니다. Nerulio의 가장자리 번짐은 알파 0 텍셀로 RGB를 2·4·8·16px 밀어 넣으며 알파는 절대 쓰지 않습니다.

- Channel PNGs측정 확인· numpy
- Normal map측정 확인· numpy
- Packed mask / ORM측정 확인· Pillow
- Engine import미검증
테두리가 생기는 곳
PNG는 색과 알파를 따로 저장하므로(스트레이트 알파) 알파가 0인 텍셀에도 RGB 값이 있습니다. 대부분의 프로그램은 거기에 (0, 0, 0)이나 (255, 255, 255)를 씁니다. 1:1에서는 알파 0이 가려서 보이지 않습니다.
필터링과 밉맵은 이웃을 섞습니다. 불투명한 가장자리 텍셀과 투명한 텍셀 사이 한가운데의 이중선형 샘플은 두 색과 두 알파를 평균하고, 밉맵 레벨은 위 레벨의 2 × 2 블록을 평균합니다. 결과의 알파는 맞게 절반이지만 색은 숨은 검정이나 흰색 쪽으로 끌려가 가장자리가 어두워지거나 빛납니다. 밉맵 레벨이 작아질수록 심해지므로 물체가 멀리 있거나 작게 그려질 때만 테두리가 보이는 경우가 많습니다.
가장자리 번짐은 숨은 색을 바꿔 줍니다. Nerulio는 한 바퀴씩 색을 넓힙니다. 이미 색이 있는 텍셀 옆의 투명 텍셀은 그런 이웃(8방향)의 RGB 평균을 받으므로, N바퀴 뒤에는 스프라이트에서 N px 안의 모든 텍셀이 가장자리 색을 가집니다. 알파가 0보다 큰 텍셀은 건드리지 않고 알파 바이트는 하나도 쓰지 않습니다. 미리 곱한 알파 텍스처는 다른 방식으로 문제를 피합니다. 색에 이미 알파가 곱해져 있어 투명 텍셀이 아무것도 더하지 않기 때문입니다.
얼마나 번지게 할까. 박스 필터 밉맵에서 레벨 k의 텍셀 하나는 원본 2ᵏ × 2ᵏ 텍셀을 덮으므로, 가장자리 블록에 섞이는 투명 텍셀은 스프라이트에서 최대 2ᵏ − 1 px 떨어져 있습니다. 따라서 4px 번짐은 1/4 레벨까지, 8px는 1/8까지, 16px는 1/16까지 깨끗하게 지킵니다. 랩의 밉맵 미리보기가 보여 주는 세 레벨입니다.
- 스트레이트 알파
- 색과 알파를 따로 저장하는 방식. 알파가 0인 곳에도 RGB가 있습니다.
- 가장자리 번짐(딜레이션)
- 투명 텍셀의 알파는 그대로 두고 가장자리 색을 바깥으로 복사하는 것.
- 밉맵 레벨
- 텍스처가 작게 그려질 때 GPU가 쓰는 미리 줄인 사본(1/2, 1/4, 1/8 …).
- 미리 곱한 알파
- 색에 알파를 이미 곱해 둔 방식. 숨은 색이 새어 나올 수 없습니다.
예시: 가장자리 블록 하나의 전후
빨간 스프라이트 텍셀 (210, 40, 30, 255) 옆에 검정이 든 투명 텍셀이 있습니다. 밉맵 레벨 1은 2 × 2 블록(불투명 둘, 투명 둘)을 평균합니다.
번짐 없음 (210+210+0+0)/4, (40+40+0+0)/4, (30+30+0+0)/4, 알파 (255+255+0+0)/4
= (105, 20, 15, 128) 반투명이면서 밝기도 절반: 어두운 테두리
번짐 있음 투명 텍셀에 이제 (210, 40, 30, 0)이 들어 있음
= (210, 40, 30, 128) 반투명, 빨강은 그대로
24 × 24 불투명 사각형 주변에서 채워지는 텍셀 수 (N px 번짐: (24 + 2N)² − 24²):
2 px → 208 4 px → 448 8 px → 1024 16 px → 25604px 값은 랩이 시험 스프라이트에서 측정한 수치와 같습니다. 텍셀 448개가 바뀌었고 모두 알파 0이었으며, 알파 바이트는 하나도 바뀌지 않았습니다.
이렇게 작동합니다
- 알파가 있는 텍셀에서 투명 텍셀로 RGB를 2/4/8/16px 밀어냅니다. 알파는 그대로.
- GPU와 같은 박스 필터로 밉맵 미리보기(1/4~1/16). 번짐이 없는 가장자리는 눈에 띄게 어두워집니다.
- 품질 리샘플러로 2의 거듭제곱 크기 변경(최근접·축소·확대·맞춤).
- 일괄 내보내기: 크기 변경 → 번짐 → 인코딩. 번짐을 마지막에 해서 크기 변경이 되돌리지 못합니다.
사용 방법
- 스프라이트나 텍스처(PNG)를 끌어다 놓습니다.
- 번지게 할 픽셀 수를 고릅니다.
- 밉맵 미리보기를 전후로 비교합니다.
- 받습니다.
결과 확인
- 보정 › 밉맵에서 번짐 뒤에는 1/4~1/16의 가장자리가 어두워지지 않고 스프라이트 색을 유지해야 합니다.
- 결과 안내에 채운 투명 텍셀 수가 나옵니다. 저장한 PNG를 채널 단계에서 열면 A 채널은 그대로이고 알파 0 아래 RGB에 가장자리 색이 들어 있습니다.
- 엔진에서 밉맵을 켠 채 물체를 작게 또는 멀리 봅니다. 테두리가 없어야 합니다.
문제 해결
| 증상 | 유력한 원인 | 확인 방법 | 해결 |
|---|---|---|---|
| 물체가 작거나 멀 때만 검은·흰 테두리가 보임 | 밉맵 레벨이 숨은 RGB를 가장자리에 섞음 | 보정 › 밉맵에서 번짐 전후의 1/4·1/8·1/16을 비교 | 보이는 가장 작은 레벨만큼 번지게 하거나(4px → 1/4, 8px → 1/8, 16px → 1/16) 엔진의 가져오기 옵션을 켜기 |
| 스프라이트가 움직이거나 회전·확대될 때 1:1에서도 얇은 테두리 | 이중선형 필터가 가장자리와 투명한 이웃 사이를 샘플링 | 엔진 화면을 확대하면 테두리가 실루엣을 따라감 | 2px 이상 번지게 하기. 도트 그림은 대신 Nearest 필터 사용(Godot에서 흐린 도트) |
| 텍스처 크기를 바꾼 뒤 번짐이 사라짐 | 크기 변경이 캔버스를 거쳐 알파 0 아래의 색을 버림 | 바꾼 파일을 채널 단계에서 열면 알파 0 아래 RGB가 다시 0 | 크기를 먼저 바꾸고 번짐은 마지막에. 랩의 내보내기는 크기 변경 → 번짐 → 인코딩 순서로 돌아감 |
| WebP·JPEG로 저장하자 번짐이 없어짐 | 이 형식은 브라우저 캔버스를 거쳐 인코딩되며 JPEG에는 알파가 아예 없음 | PNG와 WebP를 채널 단계에서 비교 | 번짐을 준 텍스처는 PNG로 보관 |
| 아틀라스에서 옆 스프라이트의 색이 가장자리에 보임 | 스프라이트를 틈 없이 패킹해 필터가 다음 스프라이트까지 닿음 | 아틀라스에서 스프라이트끼리 붙어 있는지 확인 | 번짐이 아니라 스프라이트 사이 여백·익스트루드가 필요: 아틀라스 여백, 스프라이트 시트 패커 |
| 반투명한 가장자리 픽셀 자체가 어두움 | 검은 매트 위에서 내보내 알파 1~254인 픽셀에 색이 구워져 있음 | 어두운 픽셀의 알파가 0보다 큼 | Nerulio로는 고칠 수 없음(번짐은 알파가 0보다 큰 텍셀을 건드리지 않음). 원본에서 매트 없이 다시 내보내기 |
다른 방법
| 방법 | 이럴 때 더 낫습니다 |
|---|---|
| Godot 4: Process › Fix Alpha Border (가져오기 옵션) | Godot로 가져오는 텍스처. 가져올 때 경계 픽셀을 주변 색으로 채우며 기본으로 켜져 있어, 보통은 번짐 PNG가 필요 없습니다. |
| Unity: Alpha is Transparency (가져오기 옵션) | Unity로 가져오는 텍스처. 알파 가장자리의 필터링 결함을 피하려고 색 채널을 딜레이션합니다. |
| 미리 곱한 알파 | 렌더러가 미리 곱한 텍스처로 블렌딩할 때. 숨은 색이 새어 나올 수 없습니다. |
출력 파일과 검증 방법
"측정 확인"은 내려받은 파일을 페이지 밖에서(Pillow·numpy·별도 파서) 다시 열어 그 속성을 측정했다는 뜻입니다. 게임 엔진에서 불러오는 것은 시험하지 않았습니다.
| 대상 | 파일 | 검증 |
|---|---|---|
| Channel PNGs | <name>-r/g/b/a.png (8-bit grey) | 측정 확인 numpy채널마다 바이트가 같고 완전히 투명한 텍셀 아래의 RGB도 유지됩니다. |
| Normal map | PNG · OpenGL (+Y) or DirectX (−Y) | 측정 확인 numpy디코딩한 벡터가 단위 길이(최대 오차 0.0002)이고, OpenGL과 DirectX는 초록 채널만 다릅니다. |
| Packed mask / ORM | Unity HDRP mask · URP metallic · ORM (glTF, Godot) | 측정 확인 Pillow패킹된 바이트 확인: ORM 프리셋, AO 10 / 러프니스 80 / 메탈릭 220 → (10, 80, 220, 255). |
| Engine import | Unity · Unreal · Godot | 미검증어떤 엔진에서도 불러오지 않았습니다. |
검증 근거
채널 분리가 네 채널 모두 바이트 동일(최대 차이 0), 투명 텍셀 2048개가 RGB 유지. 가장자리 번짐은 알파 0인 텍셀 448개만 바꾸고 알파 바이트는 하나도 건드리지 않음. OpenGL ↔ DirectX는 초록(255 − g)만 다름. 변환한 노멀은 단위 벡터.
한계
- 정확한 채널 바이트는 PNG에서만 보장됩니다. JPEG·WebP는 브라우저가 디코딩하며 "바이트 동일 아님"으로 표시합니다.
- 인터레이스 PNG와 16비트 출력은 지원하지 않습니다. 정확 경로는 3,355만 픽셀까지입니다.
- 노멀맵의 규약은 픽셀로 알아낼 수 없고 변환만 할 수 있습니다.
- Unity·Unreal·Godot에서 불러오지 않았습니다. 채널 순서는 각 문서와 엔진 소스에 근거합니다.
버전과 출처
텍스처 랩에서 측정(docs/TEXTURE-LAB.md): 24 × 24 사각형 주변 텍셀 448개(정확히 네 바퀴)가 바뀌었고 알파가 0보다 큰 텍셀은 없었습니다. 알파 평면은 다른 바이트 0개, 첫 바퀴는 (210, 40, 30, 0). 밉맵 미리보기는 src/game/texture-fix.js의 mipChain과 같은 채널별 박스 평균입니다. 엔진 가져오기 옵션은 Godot 4.7과 Unity 6 문서에서 인용했습니다.
참고한 공식 문서
자주 묻는 질문
투명한 부분이 보이게 되나요?
아니요. 알파 0 아래의 색만 바뀝니다. 시험에서 바뀐 텍셀 448개는 모두 알파 0이었고 알파 바이트는 하나도 바뀌지 않았습니다.
아틀라스 여백과 같은 건가요?
아닙니다. 여백은 아틀라스 속 스프라이트 사이에 공간을 두는 것이고, 번짐은 이미지 안의 투명 픽셀을 채우는 것입니다. 패킹한 아틀라스에는 둘 다 쓰세요.
무료인가요? 계정이 필요한가요?
이 페이지의 기능은 모두 무료이며 계정 없이 쓸 수 있습니다. 워터마크나 막혀 있는 내보내기가 없습니다.
스프라이트가 업로드되나요?
아니요. Studio는 이 브라우저 탭 안에서 디코딩·패킹·내보내기를 합니다. 프로젝트는 브라우저(IndexedDB)에 자동 저장되며 .nerulio 파일로 저장할 수 있습니다.
Aseprite나 엔진 플러그인을 설치해야 하나요?
아니요. .aseprite 파일은 브라우저에서 읽고 씁니다. 엔진별 번들에는 그 엔진에 필요한 것이 들어 있습니다(Godot는 가져오기 설정과 씬, Unity와 Godot 타일셋은 작은 편집기 스크립트).