LabHub
배우기 러닝패스 코스

3D 수학과 소프트웨어 래스터라이저 · 픽셀을 파일로 남긴다 · 이론

그림 파일은 왜 이렇게 생겼나

LabHub 에서 이어서 보기

한 줄 요약

이미지는 결국 숫자 배열이고, 파일 형식은 그 배열을 어떤 순서로 늘어놓고 어떻게 줄이느냐의 약속일 뿐입니다. PPM 은 줄이지 않는 쪽을 골랐고 PNG 는 줄이는 쪽을 골랐습니다.

왜 이게 필요했나

3D 그래픽스를 배우려는 사람이 처음 부딪히는 벽은 수학이 아니라 결과를 볼 방법이 없다는 것입니다. 창을 띄우려면 창 관리자가 필요하고, 그림을 저장하려면 이미지 라이브러리가 필요합니다. 실습 환경에 그 둘 다 없으면 배우기를 그만두게 됩니다.

그런데 이미지 파일 하나를 직접 쓰는 것은 생각보다 훨씬 간단합니다. 그리고 그 과정에서 얻는 것이 있습니다. image.save("a.png") 한 줄로 넘어가면 픽셀이 메모리에 어떤 순서로 놓여 있는지, 왜 폭이 홀수일 때 정렬 문제가 생기는지, 왜 알파 채널이 있으면 파일이 3분의 4로 커지는지를 끝내 모르고 지나갑니다. 그래픽스에서 이런 것들은 나중에 성능 문제로 다시 찾아옵니다.

어떻게 동작하나

PPM(Portable PixMap) 은 Netpbm 계열의 형식으로, 머리글 뒤에 픽셀을 그대로 나열합니다. 이진 형식인 P6 의 구조는 이게 전부입니다.

P6            <- 매직 넘버 (P3 이면 픽셀을 십진수 글자로 적는다)256 256       <- 폭과 높이255           <- 채널 최댓값<픽셀 바이트>  <- R,G,B,R,G,B, ... 왼쪽 위부터 오른쪽으로, 그다음 아래 줄로

픽셀은 행 우선(row-major) 으로 놓입니다. 왼쪽 위가 첫 픽셀이고, 한 행을 끝까지 채운 뒤 다음 행으로 내려갑니다. 그래서 좌표 (x, y) 의 바이트 위치는 (y * 폭 + x) * 3 입니다. y 가 아래로 자라는 것은 형식이 그렇게 정한 것이고, 3D 수학에서 y 가 위로 자라는 것과 어긋납니다. 이 어긋남을 어디서 뒤집을지는 뒤에서 계속 나오는 문제입니다.

PPM 의 문제는 브라우저가 그리지 못한다는 것입니다. 그래서 PNG 가 필요합니다. PNG 파일은 8바이트 서명으로 시작하고, 그 뒤로 길이·종류·내용·CRC 로 이루어진 청크가 이어집니다. 최소한으로 필요한 청크는 셋입니다.

89 50 4E 47 0D 0A 1A 0A   <- 서명IHDR   폭, 높이, 비트깊이 8, 색 타입 2(트루컬러)IDAT   각 행 앞에 필터 바이트 1개를 붙인 뒤 zlib 으로 압축한 것IEND   끝

핵심은 행마다 앞에 필터 바이트가 하나 붙는다는 점입니다. 필터는 이웃 픽셀과의 차이만 저장해 압축률을 올리는 장치이고, 0번은 "필터 없음" 입니다. 0번만 쓰면 인코더는 그냥 행 앞에 0을 하나 붙이는 것으로 끝납니다. 압축과 CRC 는 파이썬 표준 라이브러리의 zlib 이 둘 다 해 줍니다 — zlib.compresszlib.crc32 입니다. 그래서 PNG 인코더가 스무 줄 안에 들어옵니다.

알파 채널을 넣으면 색 타입이 6 이 되고 픽셀당 바이트가 3에서 4로 늘어납니다. 파일 크기가 3분의 4로 커지는 것이 여기서 나옵니다. 텍스처 메모리를 계산할 때 이 한 채널이 자주 문제가 되는데, 알파를 실제로 쓰지 않는 텍스처까지 네 채널로 올려 두면 GPU 메모리가 33% 더 필요합니다.

행 간격도 알아 둘 값어치가 있습니다. PNG 와 PPM 은 행이 폭 × 채널 수 바이트로 촘촘히 이어지지만, 그래픽 API 의 프레임버퍼는 행마다 4바이트나 그 배수로 정렬하는 경우가 많습니다. 그때 실제 행 간격(stride)은 폭에서 계산한 값보다 큽니다. 이 차이를 무시하고 읽으면 행이 조금씩 밀려 그림이 대각선으로 기울어집니다.

현장에서 만나는 모습

렌더러를 만들 때 사람들이 가장 먼저 겪는 버그가 두 가지인데, 둘 다 이 층에서 납니다. 하나는 그림이 위아래로 뒤집혀 나오는 것입니다. OpenGL 의 프레임버퍼는 왼쪽 아래가 원점인데 이미지 형식은 왼쪽 위가 원점이라, 읽어서 저장할 때 뒤집지 않으면 그대로 거꾸로 나옵니다. 다른 하나는 그림이 대각선으로 밀려 나오는 것인데, 이건 거의 항상 행 길이(stride)를 잘못 계산한 것입니다. 폭 × 채널 수와 실제 행 간격이 다를 때 생깁니다.

디버깅에서도 쓸모가 있습니다. 셰이더 결과가 이상할 때 중간 값(노멀, 깊이, UV)을 그대로 색으로 칠해 이미지로 뽑아 보는 것이 가장 빠른 진단법입니다. 그러려면 "지금 가진 숫자 배열을 파일로 떨구는" 함수가 손에 있어야 합니다.

색 값의 의미도 짚어 둘 필요가 있습니다. 파일에 적힌 0~255 는 대개 감마 보정된 값이지 빛의 세기에 비례하는 값이 아닙니다. 두 색의 중간을 구하겠다고 정수 두 개를 그냥 평균 내면 실제 빛의 세기로는 중간이 아닙니다. 이 실습에서는 무시하고 넘어가도 되지만, 나중에 조명 계산이 어딘가 탁해 보일 때 의심해야 할 자리가 여기입니다.

다음 실습에서 할 것

PPM 을 손으로 써서 4×4 짜리 그림을 만들고, 256×256 그레이디언트로 넓힌 다음, 같은 픽셀을 zlibstruct 만으로 PNG 로 씁니다. 그 뒤 파드 안에 작은 HTTP 서버를 띄워 웹 미리보기로 자기 그림을 봅니다. 마지막에는 원과 직선을 그려 넣고, 그림에서 센 값과 자기가 적은 값이 맞는지 확인합니다.