슈퍼 패미컴
7장 그림과 글자
화면은 8×8 타일을 이어 붙인 그림입니다. 타일 바이트를 비트평면으로 읽는 법과 팔레트, 한글 수천 자를 좁은 폰트 칸에 넣는 방법을 봅니다.
화면은 8×8 타일을 이어 붙인 그림 쉽게
슈퍼 패미컴은 화면을 픽셀 하나하나로 기억하지 않습니다. 가로세로 8픽셀짜리 작은 그림인 타일을 미리 만들어 두고, "이 칸에는 몇 번 타일"이라고 적은 타일맵으로 화면을 채웁니다. 256×224픽셀 화면이라면 타일 32×28칸입니다. 같은 타일을 여러 칸에 쓸 수 있어서 메모리가 훨씬 적게 듭니다. 욕실 타일을 붙이는 것과 비슷합니다.
롬 해킹에서 글자를 바꾼다는 것은 대개 이 타일 그림을 바꾸는 일입니다. 그러려면 타일이 바이트로 어떻게 적혀 있는지 알아야 합니다.
픽셀 하나에 비트 몇 개: 1·2·4bpp 쉽게
타일의 픽셀 하나는 색 번호입니다. 색 번호에 비트를 몇 개 쓰느냐를 bpp(bits per pixel)라고 합니다. 1비트면 0과 1, 두 가지 색이고, 2비트면 0–3, 네 가지 색입니다. 타일 하나는 64픽셀이라 bpp가 하나 늘 때마다 8바이트씩 커집니다.
| 형식 | 색 | 8×8 타일 하나 | 주로 쓰는 곳 |
|---|---|---|---|
| 1bpp | 2색 | 8바이트 | 게임이 롬에 글꼴을 줄여 넣을 때(화면에 쓰기 전에 2bpp 등으로 바꿈) |
| 2bpp | 4색 | 16바이트 | 모드 0의 모든 BG, 모드 1의 BG3(대사창에 흔함) |
| 4bpp | 16색 | 32바이트 | 모드 1의 BG1·BG2, 모든 스프라이트 |
| 8bpp | 256색 | 64바이트 | 모드 3·4의 BG1(모드 7은 평면 없이 픽셀마다 1바이트) |
그런데 2bpp 타일은 픽셀 하나의 비트 두 개를 나란히 적지 않습니다. 색 번호의 아래 비트만 모은 판과 위 비트만 모은 판을 따로 만들고, 이 판을 비트평면(bitplane)이라고 부릅니다. 한 줄(8픽셀)의 평면 0이 1바이트, 평면 1이 1바이트이고, 두 바이트가 줄마다 번갈아 놓입니다. 각 바이트의 비트 7이 가장 왼쪽 픽셀입니다.
그래서 2bpp 타일 16바이트는 "0번 줄 평면 0, 0번 줄 평면 1, 1번 줄 평면 0, …" 순서입니다. 4bpp는 이렇게 만든 16바이트(평면 0·1) 뒤에 평면 2·3이 같은 모양으로 16바이트 더 붙습니다. 아래에서 칸을 눌러 칠해 보면 어느 바이트의 어느 비트가 바뀌는지 바로 보입니다. 툴킷 tiles.py를 옮긴 코드라 툴킷과 같은 바이트가 나옵니다.
1bpp로 바꾸면 평면이 하나뿐이라 줄마다 1바이트, 모두 8바이트입니다. 4bpp로 바꾸면 그림은 같은데 평면 2·3이 모두 0인 16바이트가 뒤에 붙습니다. 같은 바이트를 다른 형식으로 읽으면 그림이 엉망으로 나오므로, 타일을 찾을 때는 형식부터 맞혀야 합니다.
색은 팔레트 번호로 고른다 쉽게
타일에 적힌 것은 색 그 자체가 아니라 팔레트 안의 번호입니다. 실제 색은 CGRAM이라는 512바이트짜리 색 메모리에 256색이 들어 있고, 2bpp 타일이라면 그중 네 색 묶음 하나를 골라 씁니다. 같은 타일도 팔레트만 바꾸면 다른 색으로 나옵니다. 배경 타일에서 색 0은 투명이라 뒤 레이어가 비쳐 보입니다.
색 하나가 2바이트라 롬이나 CGRAM 덤프에서 색을 찾을 때도 2장의 리틀 엔디안으로 읽습니다. 빨강·초록·파랑이 5비트씩이라 0–31 단계이고, 순서가 흔히 쓰는 RGB와 반대로 파랑이 위쪽 비트에 있습니다.
VRAM과 타일맵, BG 모드 쉽게
화면에 쓰는 타일 그림과 타일맵은 화면 처리 칩(PPU)의 메모리 VRAM(64KB)에 있습니다. 롬에 있는 그림은 게임이 VRAM으로 옮겨야 화면에 나옵니다. 이 옮기는 일은 화면을 그리지 않는 동안(수직 귀선 기간이나 화면을 끈 동안)에만 할 수 있어서, 보통 NMI에서 DMA로 한꺼번에 보냅니다(5장). DMA는 CPU가 한 바이트씩 옮기는 대신 하드웨어가 메모리 사이에 바이트를 한꺼번에 옮기는 기능입니다.
VRAM은 2바이트(워드) 단위로 주소를 매깁니다. 게임이 레지스터 $2116·$2117에 적는 VRAM 주소는 워드 주소이고, 바이트 주소의 절반입니다. 이 장에 나오는 VRAM 주소는 모두 바이트 주소입니다. 예를 들어 바이트 주소 $6000은 워드 주소 $3000입니다. 에뮬레이터나 문서가 어느 쪽으로 적었는지 먼저 확인해야 합니다.
타일맵의 한 칸은 2바이트입니다. 아래 10비트가 타일 번호라 한 레이어가 고를 수 있는 타일은 1,024개이고, 위쪽 비트에 팔레트 번호와 뒤집기가 들어 있습니다. 같은 타일을 뒤집어 쓰면 좌우 대칭인 그림을 반만 저장해도 됩니다.
화면의 배경 레이어(BG)는 최대 넷이고, 레이어마다 몇 색 타일을 쓸지는 BG 모드가 정합니다. 롬 해킹에서 가장 자주 만나는 것은 모드 1입니다. BG1·BG2가 16색, BG3이 4색이라, 배경은 BG1·BG2에 그리고 대사는 BG3의 2bpp 타일로 띄우는 게임이 많습니다. 제절초의 글자 레이어도 모드 1의 BG3입니다.
| 모드 | BG1 | BG2 | BG3 | BG4 |
|---|---|---|---|---|
| 0 | 4색 | 4색 | 4색 | 4색 |
| 1 | 16색 | 16색 | 4색 | — |
| 2 | 16색 | 16색 | (오프셋) | — |
| 3 | 256색 | 16색 | — | — |
| 4 | 256색 | 4색 | (오프셋) | — |
| 5 | 16색 | 4색 | — | — |
| 6 | 16색 | — | (오프셋) | — |
| 7 | 256색 | (확장) | — | — |
"(오프셋)"은 그 레이어가 그림 대신 다른 레이어의 칸별 스크롤 값을 담는다는 뜻이고, 모드 7의 "(확장)"은 BG1과 같은 그림을 128색과 픽셀별 우선순위로 한 번 더 쓰는 레이어입니다.
글꼴: 고정폭과 가변폭 쉽게
게임의 글꼴도 타일입니다. 가장 쉬운 방법은 글자 하나를 타일 한 칸(또는 2×2칸)에 그리고 타일맵에 글자 번호를 적는 것입니다. 모든 글자가 같은 폭을 차지해서 고정폭이라고 합니다. 일본어는 대부분 정사각형 글자라 이것으로 충분했습니다.
가변폭 글꼴(VWF, variable width font)은 글자마다 폭이 다릅니다. 게임이 빈 타일 여러 개를 이어 붙인 작업 공간을 두고, 글자 그림을 지금 x 위치에 비트 단위로 밀어 그린 뒤 x를 그 글자 폭만큼 옮깁니다. 글자가 타일 경계에 걸쳐도 됩니다. 같은 초오공전의 두 게임도 이렇게 비트맵에 그리는데, 각성편은 어느 글자든 x를 12픽셀씩 옮기고(그래서 결과는 고정폭), 돌격편은 글자 코드에 따라 옮기는 폭을 정합니다.
한국어 번역에서 가변폭이 반가운 이유는 두 가지입니다. 같은 창에 더 많은 글자가 들어가고, 띄어쓰기 빈칸을 좁게 만들 수 있습니다. 대신 줄바꿈 위치가 글자 폭의 합으로 정해져, 번역문을 넣을 때마다 줄 폭을 계산해야 합니다(6장의 단어 단위 줄바꿈).
한글 수천 자를 제한된 폰트 칸에 넣기 쉽게
현대 한국어로 만들 수 있는 음절은 11,172자이고, 흔히 쓰는 완성형(KS X 1001)만 해도 2,350자입니다. 한 게임의 대본에 실제로 나오는 음절은 대개 1,000자 안팎이지만, 일본판 폰트가 그만큼의 빈칸을 갖고 있는 경우는 드뭅니다. 네 패치는 저마다 다른 방법으로 자리를 만들었습니다.
안 쓰는 글자 자리에 덮어 그리기
돌격편 v1.0, 각성편
한자·가나 자리 가운데 한국어 대본에 필요 없는 칸에 한글을 그립니다. 코드 범위는 그대로라 엔진을 고칠 필요가 없지만, 칸 수가 원래 폰트 크기를 넘지 못합니다.
코드 범위를 새로 열기
돌격편 v1.1
엔진이 글자 그림 위치를 고르는 곳을 후킹해 원래 쓰지 않던 코드 범위를 빈 공간의 새 블록으로 보냅니다. 칸을 크게 늘릴 수 있습니다.
그룹 표 정리하기
제절초 (크기별로 글자를 묶어 둔 표, 아래 제절초 절)
크기가 같은 글자끼리 묶은 그룹 표가 꽉 차 있어, 아무도 쓰지 않는 글자를 정리해 그룹을 합치고 빈 칸에 새 그룹을 더합니다.
필요한 글자만 그때그때 올리기
슈퍼 마리오 RPG
모든 글자를 VRAM에 올려 둘 수 없을 때, 화면에 나올 글자만 비어 있는 VRAM 칸에 올리고 칸 번호를 기억합니다.
어느 방법이든 "이 글자 코드가 어떤 그림인가"를 적은 글자표가 함께 바뀝니다(8장). 그리고 원래 글자 칸을 덮어쓸 때는 그 글자를 쓰는 곳이 정말 없는지, 대사뿐 아니라 메뉴·이름 입력 화면·전투 메시지까지 모두 확인해야 합니다.
더 깊이비트평면 공식, 16×16 글자의 타일 배치, 1bpp 글꼴과 그림자
공식
2bpp 이상의 타일에서 (x, y) 픽셀의 색 번호는 평면 p마다 아래 바이트의 비트 (7 − x)를 꺼내 p번째 비트에 놓은 값입니다(fullsnes). 1bpp는 줄마다 1바이트라 바이트 y의 비트 (7 − x)가 곧 색입니다. 8×8 타일이면 줄 수가 8이라 평면 2·3은 16바이트 뒤입니다.
바이트 = (p ÷ 2의 몫) × 줄 수 × 2 + y × 2 + (p ÷ 2의 나머지)
색 = 평면 p마다 (바이트의 비트 7 − x) × 2^p를 더한 값8×12처럼 키가 큰 글리프도 같은 규칙에 줄 수만 다릅니다. 슈퍼 마리오 RPG의 메뉴 폰트는 8×12 2bpp라 글자 하나가 24바이트입니다.
16×16 글자는 타일 네 개
VRAM은 타일을 한 줄에 16개씩 놓은 표처럼 다룹니다. 16×16 그림은 왼쪽 위 타일 N, 오른쪽 N+1, 아래 줄 N+16, N+17로 이루어집니다. 2bpp 타일은 16바이트라 아래 줄은 위 줄보다 0x100바이트 뒤입니다. 돌격편 폰트도 이 배치로 롬에 들어 있어서, 한글판 v1.1이 새로 그린 「쫓」(코드 $CE5)은 아래 네 곳의 16바이트입니다.
폰트 시트 한 줄이 0x100바이트에 위쪽 반 8글자, 다음 0x100바이트가 그 아래쪽 반이므로, 코드 번호를 8씩 묶었을 때 짝수 번째 묶음만 글자의 시작입니다. 홀수 번째 묶음은 아래쪽 반이라 글자 코드로 쓰지 않습니다. 돌격편과 각성편 분석 노트에 같은 설명이 있습니다.
1bpp 글꼴과 그림자
롬 공간을 아끼려고 글꼴을 1bpp로 넣는 게임이 많습니다. 제절초는 크기가 다른 1bpp 글자를 뱅크 $1F에 모아 두고, 글자를 그릴 때 WRAM $7F:0010부터의 2bpp 타일 작업 공간에 OR로 겹쳐 그립니다. 평면 0이 글자(색 1), 평면 1이 그림자(색 2)입니다. 돌격편은 2bpp 글꼴에 그림자까지 그려 두었는데, 글자 픽셀은 평면 0만 1(색 1), 그림자는 두 평면 모두 1(색 3)입니다. 그림자는 글자 픽셀의 오른쪽·아래·오른쪽 아래 칸 가운데 글자가 아닌 칸입니다. 이 장의 「가」 타일도 같은 규칙으로 그렸습니다.
슈퍼 마리오 RPG의 메뉴 폰트는 롬에 8×12 2bpp로 있고, 메뉴를 열 때 게임이 외곽선과 그림자를 붙여 8×16 4bpp로 바꿔 VRAM에 올립니다($C3:6D74, 외곽선 루틴 $C3:6869).
평면을 반씩 묶어 두는 경우
어떤 게임은 2bpp 타일을 "평면 0의 8줄, 그다음 평면 1의 8줄"로 묶어 압축해 둡니다. 돌격편 타이틀 그림이 이런 경우로, 압축을 푼 뒤 $81:DFEB가 타일마다 줄 순서를 SNES 형식으로 되돌립니다. 툴킷의 planar_to_snes·snes_to_planar가 같은 변환입니다.
실제 패치에서 실제 패치에서
돌격편: 쓰지 않던 코드 범위에 512칸
돌격편의 글자 코드는 범위에 따라 시트가 정해집니다. $100–$4FF는 8×16 가나 자리, $500–$BEF는 16×16 한자 자리입니다. v1.0은 한자 850칸과 가나 160칸에 한글을 그려 1,010자를 썼고, 재번역한 v1.1에는 213자를 새로 그려야 했습니다. 그중 37자는 더는 쓰지 않게 된 옛 칸에 그리고, 나머지 176자를 위해 6장의 JML 후킹으로 원래 쓰지 않던 $C00–$FFF를 열고, 4KB씩 여덟 곳의 빈 공간을 블록으로 이었습니다. 블록 하나가 128코드(글자 64개)라 모두 512자입니다.
제절초: 그룹 표가 꽉 차 있었다
제절초 글꼴은 크기가 같은 글자끼리 묶은 그룹 표($00:F4AC)로 찾습니다. 한 칸이 5바이트이고, 표 바로 뒤에 다른 데이터가 있어 58칸이 한계인데 일본판과 v1.0이 58칸을 다 썼습니다. v1.1은 아무 데서도 쓰지 않는 168자를 이웃 그룹과 같은 크기의 빈 글자로 바꿔 45칸으로 합치고, 새 글자 75자를 3칸으로 더해 48칸으로 맞췄습니다.
슈퍼 마리오 RPG: VRAM 256칸에 다 못 넣는 이름
슈퍼 마리오 RPG의 메뉴 글자는 1바이트 코드이고, 메뉴를 열면 256글자 전부가 VRAM에 올라가 있습니다. v1.0이 0x20–0xFF를 빈칸 없이 다 써서 아이템 이름에 필요한 한글을 더 넣을 수 없었습니다. 롬 용량이 아니라 "1바이트 코드 = VRAM에 늘 올라가 있는 256칸" 구조가 막고 있었던 것입니다.
한글판은 메뉴 폰트에 없는 글자(v1.2에서 249자)를 롬 빈 곳에 두고, 화면에 나올 때만 필드 메뉴가 쓰지 않는 VRAM 바이트 주소 $6000–$7FFF(워드 주소 $3000부터, BG1 타일 $200–$2FF)의 칸 128개 가운데 하나를 배정합니다. 이 영역은 메뉴를 열 때 게임이 다시 채우므로, 슬롯 표도 그때 같이 비웁니다.
각성편: 아무도 쓰지 않는 글자 자리
각성편 v1.2에 새로 필요한 글자는 11자뿐이었습니다. 어떤 대사에서도 쓰지 않는 글리프 자리를 골라 그 위에 그렸습니다. 예를 들어 코드 $123은 v1.1까지 「겄」이었는데 v1.2부터 「깨」입니다.
직접 해 보기 직접 해 보기
툴킷의 krpatch snes tiles는 파일의 어느 바이트 범위든 타일로 그려 PNG로 저장합니다. 롬, VRAM 덤프, 압축을 푼 데이터 모두 됩니다. 한 줄에 16타일씩 그려서 위의 16×16 글자 배치가 그대로 제 모양으로 보입니다.
$ krpatch snes tiles totsugeki-k.sfc EF00 1000 block0.png --bpp 2
block0.png
$ krpatch snes tiles smrpg-k.sfc 242800 1800 menu-font.png --bpp 2 --rows 12
menu-font.png첫 명령은 돌격편 한글판의 새 글자 블록 0(4KB)을 그립니다. 둘째는 슈퍼 마리오 RPG 한글판 메뉴 폰트($E4:2800, 코드 256개 × 24바이트)를 8×12 글리프로 그립니다. 코드 $00–$1F 자리는 비어 있어서 첫 두 줄은 빈칸이고, $20부터 글자가 나옵니다. --bpp를 일부러 틀리게 줘 보면 같은 바이트가 알아볼 수 없는 무늬가 되는 것도 볼 수 있습니다. 화면에 나온 그림이 롬 어디에 있는지 모를 때는 에뮬레이터에서 VRAM과 CGRAM을 덤프해 krpatch snes bg로 레이어를 그리고, 거기서 쓰는 타일을 롬에서 찾습니다.
실제 패치에서 이 개념이 쓰인 롬 분석 노트
- 돌격편: 폰트 칸이 모자랐다
- 돌격편: 글자 칸을 512개 늘리기
- 제절초: 글자표를 눈으로 읽게 하기
- 슈퍼 마리오 RPG: 왜 v1.0은 이름을 영문으로 뒀나
- 슈퍼 마리오 RPG: 필요한 글자만 올리는 임시 칸
- 각성편: 폰트에 없는 글자
노트의 자세한 절은 「자세히 보기」를 펼친 안쪽에 있습니다.
직접 해 보기 kr-patch-toolkit 명령 설명
참고 자료
- fullsnes, SNES PPU Video Memory (VRAM) — 타일 데이터의 비트평면 배치(평면 0·1은 00h-0Fh, 평면 2·3은 10h-1Fh), BG 맵 항목 16비트, 16×16 타일 번호
- fullsnes, SNES PPU Color Palette Memory (CGRAM) — 15비트 BGR 색 형식과 CGRAM 안 팔레트 위치
- fullsnes, SNES PPU BG Control — BG 모드 0-7의 레이어별 색 수, 타일맵 위치 레지스터
- SNESdev Wiki, Tiles — 2bpp·4bpp·8bpp 형식 그림 설명
- SNESdev Wiki, Background modes — 모드별 레이어 구성과 우선순위
- SNESdev Wiki, Palettes — CGRAM과 색 0(투명)
- kr-patch-toolkit, tiles.py — 「타일을 칠하며 바이트 보기」가 옮겨 온 해독·부호화 코드(같은 테스트 사례로 확인)
댓글