도트 팔레트 고정 · 모든 프레임에 하나의 팔레트
여러 도구에서 내보낸 프레임은 팔레트가 제각각입니다. 픽셀 랩은 모든 프레임에서 하나의 팔레트를 만들어 각 프레임을 고정하고, 내보낸 모든 색이 팔레트 안에 있음을 확인합니다.
여러 도구에서 내보냈거나 크기를 바꿔 다시 저장한 프레임은 색이 조금씩 어긋납니다. 같은 명암이 미묘하게 다른 세 색이 되고 애니메이션이 반짝거립니다. 고정(lock)은 이를 바로잡습니다. 모든 프레임을 함께 보고 팔레트 하나를 만들고(2~256색, 기본 16색, 또는 PICO-8 같은 프리셋), 모든 프레임의 모든 픽셀을 거기에 맞추며, 디더링은 끄거나 순서형으로 두어 멈춘 부분이 깜박이지 않게 합니다. 픽셀 랩은 브라우저에서 이를 처리하고 프레임을 1×~8× PNG로, 팔레트를 .gpl로, 요약을 pixel-lab.json으로 내보내며, 내보낸 모든 색이 팔레트 안에 있는지 확인합니다.

- PNG frames측정 확인· Pillow
- Palette측정 확인· re-import
- pixel-lab.json미검증
프레임 색이 어긋나는 이유와, 팔레트 하나와 알맞은 디더로 막는 방법
프레임을 거치는 도구마다 색을 조금씩 바꿀 수 있습니다. 리샘플링은 이웃 픽셀을 섞고, JPEG나 손실 WebP는 잡음을 더하며, 색을 알파로 미리 곱해 저장하는 캔버스는 반투명 픽셀을 틀어지게 합니다. 이런 프레임을 하나씩 따로 양자화하면 프레임마다 팔레트가 달라집니다. 랩은 대신 모든 프레임을 알파로 가중한 히스토그램 하나에 넣고, Oklab에서 중앙값 분할로 나눈 뒤(빈도와 눈에 보이는 차이를 함께 반영) 다듬어, 모든 프레임이 함께 쓰는 팔레트 하나를 만듭니다.
깜박임은 디더링이 좌우합니다. 순서형(Bayer) 디더는 픽셀 위치에만 따르는 치우침을 더하므로, 바뀌지 않은 영역은 모든 프레임에서 같은 픽셀이 됩니다. 오차 확산(Floyd–Steinberg, Atkinson)은 반올림 오차를 프레임 전체로 넘기므로 픽셀 하나가 움직여도 그 뒤 모든 픽셀의 결정이 바뀌고, 가만히 있는 벽도 깜박일 수 있습니다. 랩의 기본값은 없음이며, 프레임이 둘 이상인데 오차 확산을 고르면 경고합니다.
랩은 캔버스를 거치지 않고 RGBA 값으로 바로 PNG를 씁니다. 캔버스는 반투명 색을 미리 곱한 알파로 반올림하기 때문입니다. 이렇게 바꾸기 전에는 16색으로 고정한 팔레트 밖으로 내보낸 색 84개 중 68개가 벗어났고, 바꾼 뒤에는 하나도 벗어나지 않았습니다.
- 팔레트 고정
- 모든 프레임의 모든 픽셀을 정해진 팔레트 하나에 맞추는 것.
- 중앙값 분할
- 목표 개수가 될 때까지 색 히스토그램을 가장 넓은 축의 중앙값에서 계속 나누는 방법.
- 순서형(Bayer) 디더
- 픽셀 위치로 정해지는 고정 문턱값 무늬. 프레임 사이에서 깜박일 수 없음.
- 오차 확산
- 픽셀마다 반올림 오차를 이웃에 퍼뜨리는 디더. 애니메이션에서 깜박일 수 있음.
예시: 안티앨리어싱된 프레임 8장을 16색으로 고정
랩의 브라우저 테스트이며, 내보낸 파일은 별도 디코더로 다시 읽었습니다.
입력 프레임 8장 × 48×48 px = 18,432 px, 안티앨리어싱된 가장자리
추출 8장 전체에서 팔레트 하나(16색)
고정 모든 픽셀 → Oklab 기준 16색 중 가장 가까운 색, 디더 없음
확인 내보낸 모든 PNG의 색: 16개, 팔레트 밖: 0개
깜박임 변하지 않는 띠: 8장에서 모두 한 가지 모양(없음, Bayer 4×4 모두)
개수 세기 전체 프레임 면적 4MP까지 정확, 여기서는 0.018MP
크기 제한 프레임 한 장당 최대 4096 × 4096 px게임에 중요한 것은 마지막 확인입니다. 미리보기가 아니라 내보낸 PNG를 열어 색을 세세요. 랩의 테스트도 Pillow로 바로 그렇게 합니다.
이렇게 작동합니다
- 모든 프레임에서 알파 가중 팔레트 하나를 만듭니다(Oklab 중앙값 분할, 2~256색). 게임보이·PICO-8·Sweetie 16·Endesga 32·NES 팔레트도 쓸 수 있습니다.
- 디더: 없음, Floyd–Steinberg, Atkinson, Bayer 2×2/4×4/8×8. 순서형 디더는 프레임 사이에서 깜박이지 않으며, 오차 확산을 고르면 경고합니다.
- 팔레트 편집: 명도·색상·채도·빈도로 정렬, 다시 추출해도 남는 색 고정.
- PNG 프레임(1×~8× 최근접), .gpl 팔레트, JSON으로 내보냅니다.
사용 방법
- 프레임을 끌어다 놓습니다.
- 색 개수나 고전 팔레트를 고릅니다.
- 디더 방식을 고릅니다(애니메이션은 기본값 없음).
- ZIP으로 내보냅니다.
고정된 프레임 확인하기
- ← →로 프레임을 넘깁니다. 같은 명암은 모든 프레임에서 팔레트의 같은 칸으로 표시돼야 합니다.
- 내보낸 PNG 두 장을 아무 이미지 편집기에서 열어 멈춘 영역의 같은 지점 색을 찍어 봅니다. 값이 완전히 같아야 합니다.
- 내보낸 프레임 한 장의 색 수를 다른 도구로 셉니다. 팔레트 크기를 넘으면 안 됩니다.
- 내보낸
.gpl을 김프나 에이스프라이트에서 불러옵니다. 같은 색이 같은 순서로 보여야 합니다.
문제 해결
| 증상 | 유력한 원인 | 확인 방법 | 해결 |
|---|---|---|---|
| 고정한 뒤 애니메이션이 깜박임 | 오차 확산 디더(Floyd–Steinberg 또는 Atkinson)가 켜져 있음 | 변환 단계에 깜박임 경고가 표시됨 | 없음이나 Bayer로 바꾸고 디더 비교 격자에서 나란히 보기 |
| 작은 디테일 색이 사라짐 | 중앙값 분할은 쓰는 픽셀 수로 색에 가중치를 주므로 2픽셀짜리 눈 하이라이트가 큰 면적에 밀림 | 추출 뒤 팔레트 단계에 그 색이 있는지 확인 | 그 색을 잠그고 다시 추출하거나 색 개수를 늘리기 |
| 끌어다 놓은 스프라이트 시트가 프레임 하나가 됨 | 랩은 이미지 한 장을 프레임 하나로 다루며 시트를 자르지 않음 | 프레임 줄에 프레임이 하나뿐 | 먼저 스프라이트 슬라이서로 자른 뒤 프레임을 놓기 |
| 랩이 이미지를 거부함 | 4096 × 4096px보다 큰 프레임은 탭을 멈추게 하므로 거부함 | 메시지에 크기와 제한이 나옴 | 먼저 줄이거나 자르거나, 프레임으로 나누기 |
| 색 개수가 한 프레임만 셈 | 전체 프레임 면적이 4MP를 넘으면 보이는 프레임만 셈 | 개수 옆 패널에 그렇게 표시됨 | 정확한 합계가 필요하면 한 번에 넣는 프레임 수 줄이기 |
다른 방법
| 방법 | 이럴 때 더 낫습니다 |
|---|---|
| Studio 픽셀 작업 공간에서 변환 | 프레임이 타임라인이 있는 레이어 스프라이트라면 색 모드…가 레이어와 태그를 유지한 채 인덱스로 바꿉니다. 팔레트 편집기 참고. |
| 추출 대신 공개된 팔레트 | PICO-8이나 Endesga 32처럼 정해진 팔레트에 맞춰야 한다면 Lospec 팔레트 적용. |
| 안티앨리어싱 가장자리와 외톨이 픽셀 정리 | 고정 뒤에도 이상한 가장자리 픽셀이나 점이 남았다면 랩의 정리 단계. 도트 정리 참고. |
출력 파일과 검증 방법
"측정 확인"은 내려받은 파일을 페이지 밖에서(Pillow·numpy·별도 파서) 다시 열어 그 속성을 측정했다는 뜻입니다. 게임 엔진에서 불러오는 것은 시험하지 않았습니다.
| 대상 | 파일 | 검증 |
|---|---|---|
| PNG frames | anim_N.png · 1×–8× nearest | 측정 확인 Pillow내보낸 PNG를 다시 읽어 모든 색이 고정 팔레트 안에 있는지 확인했습니다. |
| Palette | .gpl · .hex (Lospec) · .json | 측정 확인 re-import각 형식을 내보내고 UI로 다시 가져와 16 → 16색을 확인했습니다. |
| pixel-lab.json | generic frames + palette | 미검증일반 JSON이며 어떤 엔진에서도 불러오지 않았습니다. |
검증 근거
안티앨리어싱된 프레임 8개 → 16색 팔레트 하나, 팔레트 밖 색 0개. 디더링 없음과 Bayer 4×4 모두에서 변하지 않은 영역이 8프레임 모두 동일. 안티앨리어싱 제거로 271색 → 16색, 실루엣 픽셀 이동 0. 정수 배율·오프셋·2.5배 비정수 배율을 정확히 감지.
한계
- 애니메이션 재생은 없고 프레임을 한 장씩 넘겨 봅니다.
- 모든 처리가 메인 스레드에서 돌고 취소 버튼이 없어, 아주 큰 묶음은 끝날 때까지 탭이 멈춥니다.
- Chromium에서만 확인했습니다.
- 4096 × 4096보다 큰 프레임은 거부합니다. 모든 처리가 메인 스레드에서 돌고 아직 취소 버튼이 없습니다.
- 스프라이트 시트는 이미지 하나로 다룹니다. 먼저 프레임으로 자르세요(스프라이트 시트 자르기).
- 디더링을 켜면 정리 개수에 디더 픽셀이 포함됩니다. 먼저 디더링을 없음으로 두세요.
- 내보내는 JSON은 일반 형식이며 Godot·Unity 가져오기는 시험하지 않았습니다.
버전과 출처
Nerulio: docs/PIXEL-LAB.md. 빌드한 사이트에서 Playwright로 Chromium을 돌려 측정했고, 모든 수치는 내려받은 파일을 다시 읽어(Pillow, zipfile, json) 확인했습니다. 단위 테스트는 tests/game-palette.test.mjs입니다. 쓰는 .gpl은 김프 팔레트 형식을 따릅니다.
참고한 공식 문서
자주 묻는 질문
디더링 후 애니메이션이 깜박이는 이유는?
오차 확산은 오차를 이미지 전체로 넘기므로 한 픽셀만 움직여도 이후 결정이 바뀝니다. 순서형(Bayer) 디더는 위치에만 의존해 깜박이지 않으며, 랩의 기본값은 없음입니다.
Lospec 팔레트를 쓸 수 있나요?
네. Lospec HEX 목록, .gpl, JSON을 붙여 넣거나 가져오면 프레임이 그 팔레트에 고정됩니다.
무료인가요? 계정이 필요한가요?
이 페이지의 기능은 모두 무료이며 계정 없이 쓸 수 있습니다. 워터마크나 막혀 있는 내보내기가 없습니다.
스프라이트가 업로드되나요?
아니요. Studio는 이 브라우저 탭 안에서 디코딩·패킹·내보내기를 합니다. 프로젝트는 브라우저(IndexedDB)에 자동 저장되며 .nerulio 파일로 저장할 수 있습니다.
Aseprite나 엔진 플러그인을 설치해야 하나요?
아니요. .aseprite 파일은 브라우저에서 읽고 씁니다. 엔진별 번들에는 그 엔진에 필요한 것이 들어 있습니다(Godot는 가져오기 설정과 씬, Unity와 Godot 타일셋은 작은 편집기 스크립트).